Skip to main content
Un seul serveur OxiMail peut héberger plusieurs organisations (des tenants au sens du stockage). L’isolation n’est pas une vérification de permission ajoutée par-dessus : elle est construite dans la couche de stockage. tenant_id est un paramètre obligatoire de chaque méthode du store et chaque requête SQL filtre dessus (ADR-015). Un handler ne peut physiquement pas lire les données d’une autre organisation, même à travers un bug. Le RBAC vient par-dessus, en défense en profondeur, pas comme seule ligne. Une installation mono-organisation n’est que le cas dégénéré : l’assistant provisionne un tenant nommé default et tout ce qui suit s’applique quand même.

Les organisations

tenant delete ne supprime qu’une organisation vide, et --force confirme ce que son essai à blanc a montré : une organisation qui porte encore un compte ou un blob de portée organisation est refusée quels que soient les drapeaux (sortie 2 ; DELETE /admin/v1/tenants/{id} répond 409 avec les comptages). Les comptes ne portent pas de clé étrangère vers leur organisation, si bien que supprimer autour d’eux laissait en base chacun de ses comptes, messages et blobs, hors de portée de toute session et de tout chemin d’effacement. On supprime ou on efface d’abord les comptes ; une organisation créée par erreur et encore vide se supprime avec ses domaines et sa marque. L’état par organisation comprend ses comptes, ses domaines, ses quotas, sa clé maîtresse de tenant (qui enveloppe des secrets comme les hachages d’app passwords et les clés d’API de fournisseurs, pas du contenu, voir chiffrement au repos ; chaque secret gardé sous elle, les hachages NT des mots de passe d’application du Drive, les pré-images de réversibilité d’effacement et la clé de nœud de fédération, est scellé d’une seule façon, lié à son champ et à sa ligne, si bien qu’un blob recopié sur une autre ligne ne s’y ouvre pas), ses surcharges de configuration à l’exécution, et des réglages de politique comme :
  • Le fuseau horaire par défaut : le dernier étage de la chaîne de repli du calendrier (travail collaboratif).
  • La langue par défaut : en, fr, de ou it (tenant update --default-language, --clear-default-language, ou defaultLanguage sur le Tenant/set d’administration), proposée par l’assistant d’installation d’après le pays. La langue d’un compte suit partout une seule chaîne : celle du compte, puis celle de l’organisation, puis l’anglais. La création d’un compte se rabattait sur le français et toute autre lecture sur l’anglais ; account create --language n’a plus le français par défaut.
  • La persistance hors ligne côté client : l’organisation autorise ou non les applications clientes à persister des données localement pour le hors-ligne (offlinePersistenceAllowed, réglable par la surface d’administration Tenant/set). Une organisation aux règles strictes de résidence des données peut la couper.
  • Les règles de journalisation et la durée du tampon de réversibilité d’effacement : voir conformité.
Un secret gardé sous la clé d’une organisation est scellé pour son champ et sa ligne, et c’est la seule forme lue : un blob déplacé vers une autre ligne, ou écrit sous une autre forme, est refusé.

L’identité légale d’une organisation

Certains textes quittent le serveur vers des personnes qui n’ont pas de compte ici, à commencer par le code qui confirme une destination de transfert, et ils doivent à leur lecteur le nom du responsable et le lieu où l’adresse est traitée (textes envoyés à des tiers). Ce sont des faits déclarés, jamais déduits du réseau (ADR-164) :
  • L’organisation déclare sa nature (individual, company, association), sa raison sociale (facultative pour une personne physique), son adresse postale sans le pays, son pays d’établissement (un code ISO 3166-1 attribué aujourd’hui, pas deux lettres quelconques), la subdivision quand la loi ou l’autorité n’est pas celle du seul pays (ISO 3166-2, CA-QC, DE-BY), son autorité de contrôle, et des contacts typés (contact, dpo, representative-eu, representative-ch).
  • L’instance déclare qui l’exploite, avec les mêmes champs, plus les pays où les données sont traitées (celui du serveur principal d’abord, puis une sauvegarde ou un MX de secours à l’étranger) et le fondement de chaque transfert vers eux. Seule la console l’écrit, jamais l’administrateur d’une organisation.
  • Un champ que l’organisation laisse vide hérite de celui de l’instance à la lecture. Sans aucun contact déclaré, le contact est déduit : postmaster@ du domaine de courrier qui porte le plus de comptes de l’organisation (sur une vraie boîte, le domaine de l’organisation est souvent le nom d’hôte, où aucun postmaster@ n’existe) ; doctor vérifie que postmaster@ atteint quelqu’un sur chaque domaine qui porte des comptes.
Chaque set remplace les champs qu’il nomme et garde les autres ; --clear <champ> rend un champ de nouveau non déclaré (operator-kind, legal-name, postal-address, establishment-country, establishment-subdivision, supervisory-authority, contacts, et pour l’instance transfer-bases). Les deux sont à blanc sans --apply et écrivent une ligne d’audit dans la chaîne du locataire ; l’essai à blanc de l’instance liste les organisations dont l’identité effective changerait. L’assistant d’installation demande le pays (celui du serveur prend la même valeur par défaut) et écrit l’identité depuis la section [identity] d’un setup.toml. En JMAP, Tenant/get sert legalIdentity, ce que l’organisation a déclaré, et effectiveLegalIdentity, chaque valeur avec sa provenance (tenant, instance, derived) ; Tenant/set écrit legalIdentity en entier (null rend tout de nouveau non déclaré) et refuse une valeur hostile en nommant son champ (legalIdentity/legalName), de même que les faits propres à l’instance (API d’administration). Le pays d’établissement est aussi la juridiction de fédération de l’organisation : la cérémonie d’identité (oximail federation identity) l’énonce à côté des empreintes, oximail federation pair enregistre ce qu’elle a déclaré à chaque pair, et doctor (federation_jurisdiction) signale un pays qui a changé depuis un appairage.

Comptes et rôles

Les rôles sont admin, member, restricted, readonly, disabled (ADR-028). Un compte désactivé garde ses données mais perd son agence : pas de connexion, pas de soumission SMTP (550), pas de réponses d’absence ; le courrier continue de lui être remis. account enable --role vérifie le rôle avant d’écrire (sortie 2 sur une valeur que le serveur ne connaît pas) : une valeur inconnue était écrite, et le compte annoncé réactivé alors qu’il restait bloqué. La suppression est la machine à phases d’effacement, pas un simple delete. Un compte de service (un connecteur, par exemple un logiciel de gestion de cabinet, pas une personne) se déclare, il ne se déduit pas : oximail account create --service, ou sur un compte existant oximail admin principal set-capability avec --key service --value true sur l’objet de capacité de compte (v2:account) de son principal, qui y pose service: true. Un connecteur n’est pas un départ : un octroi d’opérateur vers lui ne porte pas d’échéance (account grant --until est refusé), doctor ne le compte pas parmi les départs et nomme comme anomalie une échéance déjà posée sur l’un de ses octrois, et son rôle est member au plus (--service avec un autre rôle est refusé, un administrateur ne peut pas être déclaré tel, et en faire un administrateur est refusé). Ce n’est pas le rôle relais service du relais sortant, une autre chose sous le même mot. Chaque compte individuel a un quota courrier et un quota Drive (50 Gio et 1 Tio par défaut) ; les comptes partagés et les boîtes système n’en ont pas, et l’organisation ne porte pas de quota de stockage. Voir les quotas pour --drive-mb, --unlimited et --default.

Mots de passe d’application

Les clients qui ne peuvent pas suivre le flux de connexion web (clients IMAP, agents DAV) s’authentifient avec des mots de passe d’application par client, générés par compte. Ils sont révocables individuellement, et les échecs alimentent fail2ban comme sur chaque écouteur. Un nouveau mot de passe d’application a huit groupes de quatre caractères (l’aspect habituel, accepté par Outlook, Apple Mail et SMB) : les deux premiers nomment sa ligne, les six autres forment un secret de 143 bits vérifié par un seul HMAC, sous une clé dérivée de la clé maîtresse de l’organisation. La première requête d’un client coûte donc une lecture et un HMAC, là où l’ancienne forme coûtait au moins cinq hachages Argon2 (environ 187 ms mesurées) ; un secret aussi long n’a pas besoin d’une fonction lente, et trouver la ligne supprime la boucle sur les mots de passe du compte, dont la longueur était l’oracle de temps. Les mots de passe d’application créés avant continuent de fonctionner sans changement, sur le chemin à travail constant, jusqu’à leur remplacement. Chaque mot de passe d’application porte un scope, choisi à la création : la CLI l’exige (pas de plein accès par défaut silencieux), tandis que POST /auth/app-password prend full quand le corps omet scope, si bien qu’un client doit toujours en envoyer un. Les scopes sont full, mail, calendar (qui inclut les tâches : un seul credential d’agenda couvre les clients CalDAV VTODO), contacts (le credential de synchronisation CardDAV), drive, ou une liste personnalisée d’URN de capabilities en tableau JSON. Un scope nommé accorde aussi les capabilities OxiMail de la surface qu’il couvre : mail accorde urn:oximail:params:jmap:v2:mail, v2:labels et v2:threads, si bien qu’un credential courrier envoie par Email/send en JMAP comme il le pouvait déjà par SMTP ; calendar accorde v2:calendar, contacts accorde v2:contacts. Une surface à part ne vient qu’avec full ou une liste personnalisée, jamais avec un scope nommé : v2:rules reste hors de mail pour la raison qui y tient Sieve (une règle peut rediriger, et un credential fuité s’offrirait une redirection qui survit à sa révocation), de même que v2:admin et v2:conversation. Une liste personnalisée est validée à la création sur les deux surfaces : chaque élément doit être une capability que ce serveur sert réellement, et le refus nomme les entrées fautives. Un credential qui revendiquerait un droit que le dispatch n’honorera jamais ne peut pas être créé. La CLI (oximail app-password create) valide contre le vocabulaire intégré et le dit ; pour scoper un credential sur une capability d’extension enregistrée au démarrage, passez par POST /auth/app-password, qui les connaît aussi. Un mot de passe d’application peut être en lecture seule (oximail app-password create --read-only, ou readOnly: true sur POST /auth/app-password), le credential d’un assistant qui ne doit pouvoir ni supprimer, ni déplacer, ni envoyer. Il n’ouvre que JMAP : IMAP, SMTP, CalDAV/CardDAV et SMB le refusent à la connexion, puisqu’un credential en lecture seule n’est tenu que là où existe le refus d’écriture de JMAP (un credential drive en lecture seule ne reçoit pas de hash NT pour SMB). En JMAP, la session présente le compte en lecture seule et chaque méthode qui écrit répond accountReadOnly, quel que soit le scope ; le credential ne peut pas non plus frapper de mots de passe d’application. Un compte désactivé reste désactivé : le drapeau ne fait jamais que restreindre. Un administrateur voit et coupe les mots de passe d’application et les sessions d’appareil d’un autre compte depuis la surface d’administration comme depuis la console (app-password list|revoke) ; couper un mot de passe d’application ferme chaque token bearer qu’il a ouvert (API d’administration). Les passkeys (WebAuthn) suivent la même discipline de gestion : GET /auth/passkey et DELETE /auth/passkey/{id} permettent à un utilisateur connecté de lister et révoquer ses credentials, et oximail passkey list|revoke est la porte hors bande de l’opérateur. C’est voulu : quand le credential à révoquer est celui qui ouvre le webmail, le chemin de révocation ne doit pas être le webmail. Le listing inclut pour chaque credential l’indice discoverable (a-t-il été créé utilisable par une connexion sans nom d’utilisateur), enregistré à l’enrôlement.

Comptes d’équipe partagés

Un compte partagé est une identité d’équipe non connectable (comptabilite@, equipe@) qui possède une boîte et des ressources d’équipe sans être une personne :
Personne ne s’y connecte ; les humains l’atteignent par des droits. Trois invariants gardent le modèle honnête :
  • Un compte partagé ne peut jamais être bénéficiaire. Une entrée shareWith dont le principal est adossé à un compte partagé est rejetée à l’écriture sur tous les domaines : une identité non connectable ne peut jamais exercer un droit, l’accepter créerait des entrées d’ACL mortes.
  • Son drive a toujours un gardien. La racine de fichiers du compte partagé n’est pas modifiable en JMAP ; la garde est une surface CLI explicite. oximail account custodian show liste les gardiens actuels et indique si le repli sur les administrateurs humains du tenant tient ; custodian reattach --principal <p> [--replace] accorde à un principal adossé à un humain les pleins droits sur la racine, avec audit (l’état antérieur est conservé dans l’entrée d’audit, un transfert est donc restaurable). Les commandes qui modifient les rôles refusent de retirer le dernier administrateur humain de l’organisation tant qu’un drive partagé n’a pas de gardien explicite.
  • Dans l’annuaire, il est typé honnêtement. Le principal d’un compte partagé est émis en type: "other", pas individual : les sélecteurs de personnes ne le proposent pas comme un humain.

Annuaires de contacts partagés

Une organisation peut publier des jeux de contacts partagés (une liste de clients, une liste du personnel) qui apparaissent dans le carnet d’adresses de chaque membre sans que personne ne recopie de fiche. C’est un objet du plan d’administration : il est créé et nommé par un administrateur de tenant, et les entrées du quotidien sont tenues par les principaux inscrits comme éditeurs, groupes compris. Deux conséquences comptent pour l’exploitation :
  • Les membres reçoivent des contacts, pas un dossier. Les entrées arrivent chez un membre en fiches lisibles seulement, marquées comme provenant de l’organisation, sur le plan même qu’utiliserait un flux LDAP ou SCIM externe. Il n’y a rien qu’un utilisateur puisse renommer, déplacer ou supprimer par mégarde, et rien à départager quand quelqu’un s’en va.
  • Le contenu appartient à l’organisation, pas au membre. Un annuaire se purge en retirant ses entrées ; il ne suit pas l’effacement d’un compte individuel, puisqu’il n’a jamais été la donnée de cette personne.
La surface est servie en JMAP pour les sessions d’administration et d’édition. Il n’existe volontairement aucun verbe en ligne de commande : c’est du contenu d’annuaire, pas de la configuration de serveur. Le détail des méthodes et des propriétés est sur la page contacts.

Les entités d’une organisation

La carte d’annuaire d’une personne peut nommer son employeur. Pour que « ACME SA » soit une seule chose et non cinq graphies, l’employeur est une entité : un groupe que l’administrateur a déclaré tel, jamais déduit (chaque groupe a un compte d’adossement, si bien que « a un compte » ferait de chaque équipe et de chaque groupe de partage un employeur). Une carte cite une entité par référence et lit son nom sur le groupe, si bien que renommer le groupe corrige toutes les cartes ; retirer la marque d’une entité que des cartes citent encore, ou la détruire, est refusé avec le nombre.
  • create fait naître un groupe déjà entité, avec son propre compte d’adossement passé par le provisionnement commun (le plafond de comptes de l’organisation, un domaine possédé, une adresse unique) ; un nom que porte déjà un groupe de l’organisation est refusé en le nommant. Sans --email, l’adresse est générée (grp-<id>@<domaine primaire>) et ne reçoit rien tant que sa surface courrier n’est pas posée (Principal/enableMail, partage).
  • set-entity déclare un groupe existant entité (--value true) ou retire la déclaration (--value false), avec les mêmes refus que l’écriture JMAP et une ligne d’audit (principal/set-entity).
  • list classe ce que portent les cartes : les graphies libres à rattacher (chacune désignée par une empreinte de huit caractères, jamais par sa chaîne ni par un rang : une graphie mesurée en production portait trois espaces finaux), les références saines, les dégradées, et les entités qu’aucune carte ne cite. Il nomme l’état du compte derrière chaque carte (actif, fermé avec sa date de purge, ou sans compte) et met à part les graphies que seuls des comptes fermés portent. Lecture seule ; sortie 1 sur une référence pendante ou qui vise un groupe qui n’est plus une entité.
  • set rattache à l’entité chaque carte qui porte exactement cette graphie, cartes des comptes fermés comprises, et dit quels comptes sont fermés. La carte d’un compte fermé est réécrite, pour qu’une réouverture montre l’entité, mais pas publiée (attached-not-published). Il ne devine aucun rattachement : la décision est humaine, le verbe l’applique.
Un compte fermé n’est pas dans l’annuaire des personnes : le fermer (une désactivation, une retenue par politique, le début d’un effacement) retire sa carte du carnet de chaque collègue, et le rouvrir la publie de nouveau, puisqu’elle est restée sur le compte. oximail doctor avertit (directory_closed_accounts) tant qu’un compte fermé est encore publié, par exemple un compte fermé avant cette règle ; oximail admin profile backfill le retire alors (would-withdraw, puis withdrawn avec --apply), et publie comme avant la carte des comptes ouverts qui n’en ont pas. Chaque verbe qui écrit est un essai à blanc sans --apply. Le côté JMAP (isEntity, cardReferenceCount, le filtre de Principal/query) est sur la page partage.

Plafonds de capacité

La capacité du chat est bornée par organisation via la section TOML [chat], et les valeurs appliquées sont annoncées dans l’objet de capacité du chat, pour que les clients les respectent avant de rencontrer l’erreur : Ces plafonds bornent le produit d’écriture de la diffusion (chaque message de salon écrit une ligne par membre).

Un serveur, plusieurs organisations : les frontières

  • Les domaines de courrier appartiennent aux organisations ; l’adresse d’un compte doit être dans un domaine que son organisation possède.
  • Les blobs sont stockés sous des chemins par organisation, et la ligne de registre qui nomme l’organisation propriétaire est la seule autorité qu’une lecture consulte ; la déduplication inter-organisations n’existe pas, à dessein.
  • Les destinations de journalisation, les droits de partage et chaque objet JMAP se résolvent strictement dans l’organisation.
  • L’API d’administration et la CLI opèrent par organisation (--tenant-id) ; il n’existe volontairement pas de vue de données « tous tenants ».

L’opérateur d’instance

Quelques choses décrivent la box, pas une organisation, et en nomment plusieurs : le relevé périodique de doctor (HealthReport/query|get, voir l’API d’administration) cite des adresses, des domaines et des identifiants de compte de chaque tenant, et le total de comptes de GET /health dit combien de comptes portent les autres. Sur une box où une organisation est seule, ses administrateurs lisent les relevés pris pendant qu’elle était seule, sans rien poser. Sur une box qui en porte plusieurs, ce qui nomme une autre organisation est lu par un opérateur d’instance, un privilège hors de toute organisation, posé à la console seulement :
  • Le porteur est une personne qui administre sa propre organisation. grant refuse un compte qui n’est pas un administrateur humain de son tenant, ou qui n’a aucun second facteur (passkey) enrôlé, en nommant ce qui manque ; --by est la personne qui décide (un nom, jamais une adresse), écrite sur la ligne d’audit sous le porteur. Les deux verbes sont des essais à blanc sans --apply.
  • Vérifié à chaque appel, jamais déduit. Le privilège ne tient que tant que le compte a sa ligne, est encore administrateur et a encore son second facteur ; un compte désactivé ou rétrogradé, ou dont la passkey a disparu, garde une ligne inerte qui n’ouvre rien, et l’effacement du compte emporte la ligne. doctor (instance_admin) nomme chaque ligne inerte avec sa raison, et avertit sur une box à plusieurs organisations sans porteur actif.
  • JMAP le lit et ne l’écrit jamais. Aucune valeur de rôle ne le désigne, donc aucune route qui écrit un rôle ne peut le poser. Un retrait prend effet dans les 30 secondes, le cache de capacités du serveur.
  • Le porteur est protégé des administrateurs de sa propre organisation. Poser son mot de passe, toute mise à jour Principal/setAdmin de son principal, couper ses sessions d’appareil ou ses mots de passe d’application depuis la surface d’administration sont refusés forbidden : on agit à la console, après avoir retiré le privilège.
  • Il ouvre une liste fermée : chaque HealthReport, et le total de la box dans GET /health. Il n’hérite de rien d’autre, en particulier pas des journaux d’audit des autres organisations.