Bibliothèque de playbooks

Isolation des données multi-tenant

L'isolation des tenants est la propriété qu'aucun test manuel ne met en évidence : tout paraît correct quand on est connecté à un seul compte. Elle se dégrade aussi en silence — une nouvelle requête sans le filtre, une clé de cache sans le préfixe du tenant, un worker qui a hérité du mauvais contexte. Ces règles existent pour faire de chacun de ces cas un échec visible au niveau du diff.

Écrit pour
Les équipes SaaS dont une seule base sert de nombreux clients — y compris les agences qui gèrent des workspaces clients.
Non couvert
La row-level security en base est une garantie plus forte que tout ce qui suit, et elle vaut la peine d'être adoptée si vous le pouvez. Ces règles protègent les chemins de code que la RLS ne couvre pas — et c'est ce dont vous avez besoin en attendant.

10 règles7 bloquantes3 warning

Démarrer un playbook gratuit →

01 L'identifiant de tenant vient de la session

error

L'identifiant de tenant (ou d'organisation) est lu depuis la session authentifiée ou un token vérifié, et de nulle part ailleurs. Le prendre dans le corps de la requête, la query string, un paramètre de chemin ou un en-tête fourni par le client est refusé, même si le code contrôle ensuite l'appartenance. Tout identifiant de tenant que le client peut envoyer est un identifiant de tenant que le client peut changer.

Bloque : c'est la racine de tout le modèle d'isolation — une seule exception annule toutes les autres règles.

Portéetout le repo

Tagsmulti-tenancysecurityauthorization

02 Chaque requête filtre sur le tenant

error

Chaque select, update et delete sur une table scopée par tenant inclut le prédicat de tenant — y compris les accès par clé primaire. « L'id est un UUID, donc il est indevinable » n'est pas un contrôle d'autorisation : les identifiants fuient par les exports, les logs, les tickets de support et les URL partagées.

Bloque : un seul prédicat manquant, et les données fuient d'un client à l'autre.

Portéetout le repo

Tagsmulti-tenancysecuritydatabase

03 Les clés de cache portent l'identifiant de tenant

error

Toute clé de cache dérivée d'un id d'entité, d'une saisie utilisateur ou d'un résultat calculé inclut l'identifiant de tenant. Une clé qui ne le fait pas sert les données d'un client à un autre, et l'entrée empoisonnée survit au déploiement qui a introduit le bug — le correctif ne corrige donc rien.

Bloque : les fuites de cache traversent les clients, persistent et n'apparaissent pas dans les logs.

Portéetout le repo

Tagsmulti-tenancysecuritycaching

04 Les chemins de stockage sont préfixés par le tenant

error

Les fichiers déposés et générés sont écrits sous un préfixe scopé par tenant, et toute URL signée est émise avec une expiration courte (quinze minutes au plus). Un espace de noms de bucket à plat signifie qu'une clé devinable ou énumérable donne un accès direct au document d'un autre client, sans qu'aucun code applicatif n'intervienne.

Bloque : le stockage objet contourne tous les contrôles qui vivent dans votre application.

Portéesrc/**/storage/**src/**/*upload*src/**/*file*

Tagsmulti-tenancysecuritystorage

05 Les jobs portent le tenant explicitement

error

Un job mis en file ou une tâche planifiée inclut l'identifiant de tenant dans son payload, et le handler rétablit le périmètre à partir de là avant de toucher aux données. S'appuyer sur un contexte ambiant ou local au thread à l'intérieur d'un worker est refusé — le contexte qui a produit le job a disparu depuis longtemps quand celui-ci s'exécute.

Bloque : un worker mal scopé écrit chez le mauvais client, en silence.

Portéesrc/**/jobs/**src/**/workers/**src/**/*worker*

Tagsmulti-tenancysecurityqueue

06 Une nouvelle table scopée porte la colonne et l'index

error

Une nouvelle table qui contient des données clients déclare une colonne de tenant non nulle, avec une clé étrangère vers la table des tenants, et un index qui commence par cette colonne. L'ajouter plus tard impose une migration sur une table remplie, plus un audit de toutes les requêtes écrites entre-temps.

Bloque : le coût d'une erreur ici grandit chaque jour où la table existe.

Portéemigrations/**db/migrations/****/schema*.ts

Tagsmulti-tenancydatabase

07 L'attribution d'un rôle vérifie le tenant cible

error

Avant de créer une invitation ou de changer un rôle, vérifiez que l'acteur est bien membre du tenant de la session avec le droit d'attribuer, et que l'utilisateur cible comme le rôle cible appartiennent à ce même tenant. L'élévation de privilèges entre tenants est le bug le plus grave de cette catégorie de produits, et son chemin de code a l'air parfaitement ordinaire.

Bloque : cela offre à un attaquant un accès durable, et d'apparence légitime, aux données d'un autre client.

Portéesrc/**/members/**src/**/invitations/**src/**/*role*

Tagsmulti-tenancysecurityauthorization

08 Les lectures inter-tenants sont explicites et tracées

warning

Une requête qui traverse volontairement les tenants — outillage d'administration, agrégats de facturation, analytics internes — passe par un helper non scopé et nommé comme tel, et journalise l'acteur qui l'a lancée. Sans sortie de secours identifiée, le filtre disparaît discrètement dans du code ordinaire, et les deux cas deviennent indiscernables en revue.

Avertit : les usages légitimes existent, l'objectif est donc la visibilité — le reviewer confirme que la sortie de secours était voulue.

Portéetout le repo

Tagsmulti-tenancyaudit

09 Une ressource d'un autre tenant renvoie 404, pas 403

warning

Une ressource appartenant à un autre tenant renvoie exactement ce que renvoie une ressource inexistante. Un 403 confirme que l'id existe, ce qui transforme n'importe quel endpoint en oracle d'énumération sur le volume de données, la nomenclature et l'activité d'un autre client.

Avertit : cela fait fuiter des métadonnées plutôt que du contenu, mais c'est ce qui rend les attaques ciblées bon marché.

Portéesrc/routes/**apps/api/**

Tagsmulti-tenancysecurityapi

10 Livrez le test inter-tenants avec l'endpoint

warning

Chaque nouvel endpoint scopé par tenant est livré avec un test qui vérifie qu'un membre du tenant B reçoit un 404 sur une ressource du tenant A. L'isolation est la propriété que le test manuel n'exerce jamais, parce qu'un humain n'est connecté qu'à un seul compte à la fois.

Avertit : des tests manquants ne cassent pas la production aujourd'hui — ils garantissent seulement que personne ne s'en apercevra le jour venu.

Portée**/*.test.***/*.spec.*tests/**

Tagsmulti-tenancytesting

Avant de tout importer

La sévérité est le levier qui décide si l'équipe garde la gate activée. Seul error bloque un merge — importez d'abord ces règles en warning, regardez ce qui se déclenche réellement sur quelques semaines de vraies pull requests, et promouvez celles qui l'ont mérité. Et réécrivez les globs de portée : les chemins ci-dessus sont des exemples, et une règle ne s'applique qu'aux fichiers correspondant à ses globs.

Les règles entrent dans Normik une par une — via le formulaire du playbook, ou en laissant un agent de code les proposer par MCP à partir d'un repo existant. Il n'y a pas d'import markdown en masse : le bouton de copie ci-dessus sert à votre AGENTS.md, à une PR où l'équipe débat de la liste, et à garder les règles sous les yeux pendant que vous saisissez celles que vous avez retenues.

Chaque règle ici est jugée comme Normik la juge : sur les lignes que la pull request ajoute. Rien dans ce playbook ne dépend de la description de la PR, d'un fichier que le diff ne touche pas, ni de l'historique d'un fichier — ce sont de vrais standards de revue, mais une gate qui n'en voit pas la preuve ne pourrait que rester muette ou deviner.

L'anatomie d'une règle, le comportement des sévérités et des globs, le versionnage et l'export AGENTS.md sont détaillés dans la documentation du playbook.

Vos agents codent déjà vite. Faites-les coder comme votre équipe.

Commencer gratuitement — sans carte bancaire