security
Multi-tenant au niveau ligne, et le seul bug qui compte
Multi-tenant au niveau ligne, et le seul bug qui compte
Au lieu d'une base ou d'un schéma séparé par tenant, chaque table appartenant à un tenant porte un tenant_id (parfois un branch_id aussi), et chaque requête est censée filtrer dessus. Un seul schéma. L'isolation imposée entièrement dans la logique applicative plutôt que la structure de la base qui garde structurellement les tenants séparés.
Le niveau ligne est nettement plus simple à exploiter — un seul jeu de migrations, un seul pool de connexions, des requêtes d'admin et de l'analytique cross-tenant triviales, monte à des milliers de tenants sans provisionnement par tenant. Le schéma-par-tenant te donne une isolation structurelle, où un bug ne peut littéralement pas fuir entre tenants parce que les données vivent quelque part que la requête ne peut pas voir, au prix d'une complexité de migration multipliée par le nombre de tenants et d'opérations cross-tenant bien plus dures.
Pour une plateforme qui attend beaucoup d'organisations avec une personne qui la maintient, le niveau ligne est le bon compromis. Mais la garantie d'isolation n'est aussi solide que la discipline de chaque requête.
La classe de bug
Comme il n'y a pas de barrière structurelle, l'isolation est exactement aussi solide que « chaque requête a-t-elle pensé à filtrer par tenant_id ». Rate-le une fois — un endpoint, un rapport, un outil d'admin — et les données d'un tenant sont lisibles ou modifiables par un autre. C'est la classe de bug la plus grave dans un système au niveau ligne, et elle est silencieuse : rien ne crashe, ça renvoie ou modifie juste discrètement les lignes du mauvais tenant.
Des garde-fous qui aident vraiment
- Cadre à la couche query-builder ou repository, pas par site d'appel. Une méthode de base qui injecte toujours
tenant_iddepuis le contexte de la requête authentifiée est bien plus sûre que de faire confiance à chaque contrôleur pour s'en souvenir. - Une couche de politique dédiée qui vérifie la propriété du tenant avant toute lecture ou écriture, pas après avoir récupéré. Dans ma plateforme c'est
core/policy/, pour que la logique de cadrage ne soit pas réinventée dans chaque module. - L'admin cross-tenant et l'usurpation sont un chemin délibérément séparé et audité — pas un contournement caché dans la logique de requête normale. L'usurpation est sa propre table avec des jetons de confirmation et une piste d'audit complète, pas un drapeau d'admin qui saute le filtre.
- Teste toujours avec deux tenants côte à côte. Une suite qui ne seed que les données d'une organisation n'attrapera jamais un filtre manquant — la requête renvoie « par accident » des résultats corrects en apparence sans rien d'autre présent pour fuir.
La même discipline joue aussi verticalement, pas seulement de côté. Les actions d'un admin de succursale doivent pouvoir être outrepassées par des rôles au niveau organisation ou plateforme, parce qu'une décision au niveau succursale pourrait être une vraie erreur qu'une autorité supérieure doit pouvoir corriger. C'est le raisonnement derrière la hiérarchie de paramètres plateforme-vers-organisation-vers-succursale : isolation latérale entre tenants, et un chemin d'outrepassement contrôlé vers le haut dans la chaîne de privilèges. Les deux directions doivent être conçues exprès, pas découvertes comme des trous plus tard.