Aller au contenu

Module XX — multi-tenancy et gestion des organisations

Le module XX n’est pas un service greffé sur le moteur — c’est une propriété du moteur lui-même. Il n’existe pas de module de tenancy distinct à attacher ; au contraire, le modèle de données du cœur porte une frontière de tenant sur chaque entité et le store l’applique sous chaque requête. Cette page est la référence de ce que cette frontière garantit aujourd’hui, et des parties de la gestion des organisations qui en sont encore au stade de la conception.

La multi-tenancy vit dans la couche Engine (couche 0), aux côtés de l’API propre à la plateforme (module XIX), car réintégrer l’isolation dans un modèle de données déjà en service est précisément le genre de changement qu’on ne peut pas faire en sécurité plus tard. Chaque entité du cœur porte un tenant_id, et un appelant n’en passe jamais un comme paramètre libre : il fixe le tenant une seule fois et reçoit un scope dont les dépôts sont déjà liés à celui-ci. Il n’existe aucun vocabulaire dans l’API pour traverser les tenants — cette absence est la première barrière d’isolation, avant tout mécanisme de base de données. Le scope privilégié inter-tenants (créer une organisation, lister les organisations, supprimer un tenant) n’est accessible que par le démarrage propre du moteur, jamais par un module.

Le modèle de tenant appartient au contrat du modèle de données, non à un schéma propre à chaque module. L’entité racine est l’Org, qui est le tenant : lorsque le moteur amorce une organisation, son identifiant devient l’identifiant du tenant et la chaîne d’audit propre à l’organisation est établie au même moment. Toute autre entité du cœur — agents, sessions, ressources, identités, politiques, enregistrements de coûts, findings, déploiements, l’access map et l’audit ledger — est créée à l’intérieur d’un scope de tenant et estampillée avec ce tenant à l’écriture ; l’appelant ne peut pas le surcharger.

L’isolation est appliquée au niveau de la couche de requêtes, selon le déploiement :

  • Sur PostgreSQL, chaque table portant tenant_id s’exécute sous FORCE ROW LEVEL SECURITY avec une politique tenant_isolation liée par transaction. Une transaction qui ne parvient pas à lier un tenant lève une erreur plutôt que de renvoyer silencieusement zéro ligne (fail-closed). Le rôle applicatif est non-superutilisateur et n’a jamais BYPASSRLS, et FORCE lie la politique même pour le propriétaire de la table. La propriété, elle, est un choix de déploiement : l’installation mono-rôle par défaut laisse le rôle applicatif propriétaire de la base — la RLS le lie toujours, mais un propriétaire peut altérer ses propres tables, de sorte que cette posture est capable de détecter les altérations plutôt qu’à l’épreuve du propriétaire. La frontière dure de privilège — un rôle applicatif qui est aussi non-propriétaire — vient de la topologie scindée propriétaire/application, où un rôle propriétaire distinct exécute le provisionnement et le rôle applicatif ne reçoit que le DML dont il a besoin.
  • Sur SQLite (le déploiement mononœud) il n’y a pas de sécurité au niveau ligne ; l’équivalence vient de deux faits — le seul chemin vers la base est le SQL généré par descripteur qui ajoute toujours le prédicat de tenant, et des déclencheurs tripwire annulent toute écriture dont le tenant ne correspond pas au scope épinglé.

Un auto-test de démarrage interroge les garde-fous d’isolation en vigueur après la migration et refuse d’ouvrir le store si une table portant tenant_id n’est pas protégée — de sorte qu’un garde-fou oublié sur une nouvelle table devient un échec de démarrage, et non une fuite silencieuse.

Le module XX n’a aucune surface de bus d’événements ni aucune actuation. Il ne consomme pas edge.observed, n’émet pas de findings et n’appelle aucun fournisseur — c’est le substrat à travers lequel les autres modules écrivent. Son seul effet observable est structurel : chaque entité qu’un module persiste est déjà scopée par tenant, et chaque mutation sur une entité auditée s’ajoute à l’audit ledger chaîné par hash de ce tenant au sein de la même transaction.