Multi-tenancy sécurisée dans Lino

Multi-tenancy permet à une seule application de servir plusieurs clients, entreprises ou organisations tout en gardant les données et les permissions isolées. Dans Lino, Tenant est traité comme une partie du flux de l'application : la fonctionnalité génère la base de tenancy, résout le Tenant par sous-domaine, host ou slug via ITenantContext et expose l'utilisateur authentifié par le JWT via IUserContext.

Le JWT porte également le TenantId pour lequel l'utilisateur a été authentifié. Lino compare ce Tenant avec le Tenant résolu pour la requête ; cette page se concentre donc sur ce que vous devez décider après l'existence de cette base : modélisation des entités, permissions par organisation et utilisation correcte des abstractions générées.

Ajout du support Tenant

Utilisez la fonctionnalité Tenant lorsque le système est un SaaS ou lorsque les données de différents clients doivent coexister dans la même application sans fuite entre contextes.

lino feature tenant add

L'assistant génère la base pour l'enregistrement des Tenants, l'association des utilisateurs, la résolution du Tenant actuel, la propagation dans le contexte de l'application et l'intégration avec l'authentification. Le ITenantContext représente le Tenant résolu par sous-domaine, host ou slug, tandis que le IUserContext représente l'utilisateur authentifié par le JWT, y compris le TenantId pour lequel il a été authentifié.

Après la génération, vérifiez quelles entités appartiennent réellement à un Tenant et lesquelles sont globales. Cette revue fait partie de la modélisation du produit : multi-tenancy ne consiste pas seulement à créer une table de Tenants, mais à décider quelles règles, écrans, permissions, données et opérations dépendent du Tenant actuel validé par Lino.

  • Tenant-scoped : commandes, produits d'un client, paramètres d'entreprise, clients d'un CRM ou toute donnée appartenant à une organisation spécifique.
  • Global : catalogue public de la plateforme, plans, ressources internes d'exploitation, paramètres administratifs ou données utilisées avant le choix d'un Tenant.
  • Hybride : données globales avec personnalisation par Tenant, qui exigent une modélisation explicite pour éviter une duplication incorrecte ou une fuite de règles.

Dans un monolithe modulaire, le module de tenancy est généralement propriétaire de l'entité Tenant d'origine. Les autres modules, comme le catalogue ou le CRM, ne doivent pas accéder directement à cette entité. Lorsqu'ils doivent connaître le Tenant, ils peuvent conserver des shadow entities avec les champs minimum nécessaires, alimentées par des événements d'intégration comme la création ou la mise à jour de Tenant.

Tenant context

Avec la fonctionnalité Tenant, Lino centralise la résolution du Tenant actuel et expose cette valeur via ITenantContext. Ce contexte représente le Tenant résolu à partir du sous-domaine, host ou slug de la requête, y compris des informations comme TenantId et le slug lorsque le Tenant a été trouvé.

Dans les flux authentifiés, IUserContext lit l'utilisateur depuis le JWT. Ce JWT contient également le TenantId pour lequel l'utilisateur a été authentifié. Lino compare le Tenant de IUserContext avec le Tenant de ITenantContext, empêchant les règles métier de faire confiance à des contextes divergents.

Dans les fonctionnalités générées ou ajoutées ensuite, préférez toujours ces abstractions au lieu de relire manuellement les claims ou de recevoir un TenantId arbitraire dans chaque handler. Cela maintient commands, queries, pages et intégrations sur la même source de vérité et préserve les mécanismes de sécurité déjà implémentés par Lino.

Les Background Jobs, consommateurs d'événements et intégrations ont également besoin de contexte. Un job qui traite des produits, clients ou utilisateurs tenant-scoped doit recevoir explicitement quel Tenant est traité, car il n'existe aucune requête HTTP active depuis laquelle ITenantContext et IUserContext pourraient être inférés automatiquement.

Entités tenant-scoped

Les entités tenant-scoped portent l'identifiant du Tenant et participent aux filtres de requête. Ainsi, le flux courant n'a pas besoin de se souvenir manuellement d'appliquer TenantId dans toutes les queries ; la fonctionnalité doit utiliser le contexte validé et les filtres générés par Lino.

  • Incluez Tenant dans les aggregates qui appartiennent à une organisation spécifique.
  • Configurez les index en tenant compte de Tenant et des champs fréquemment consultés.
  • Utilisez ITenantContext et IUserContext dans les commands et queries au lieu de recevoir Tenant comme autorité isolée.
  • Dans les imports et les jobs, définissez explicitement quel Tenant est traité.

Une entité tenant-scoped appartient à un Tenant. Si une entité semble appartenir à plusieurs Tenants, traitez cela comme une décision de modélisation : elle est peut-être globale, elle doit peut-être exister en copie par Tenant, ou il manque peut-être une entité intermédiaire qui représente la relation. Un produit, client ou utilisateur peut aussi avoir un sens différent selon les modules ; dans ces cas, une shadow entity avec peu de champs peut être plus correcte que le partage de l'entité d'origine.

Distinguez également référence et ownership. Un produit dans le catalogue peut référencer le Tenant, mais cela ne signifie pas que le catalogue contrôle le cycle de vie du Tenant. Le module de tenancy reste propriétaire de la création, de l'activation, du domaine, du branding et du mode d'isolation ; le catalogue conserve uniquement les données nécessaires pour filtrer et valider son propre flux.

  • Index : combinez TenantId avec les champs utilisés pour la recherche, le tri et l'unicité par Tenant.
  • Commands : appliquez l'opération dans le Tenant actuel validé lorsque l'entité est tenant-scoped.
  • Queries : gardez la pagination, les filtres et les compteurs dans le même contexte validé par Lino.
  • Événements : incluez suffisamment de contexte pour que les consommateurs mettent à jour les projections sans interroger indûment le producteur.

Isolation des données

L'isolation peut être faite par colonne, schema ou base de données séparée. Le choix dépend du coût, du volume, des exigences contractuelles et de la capacité opérationnelle.

StratégieAvantagesQuand l'utiliser
Colonne TenantIdPlus simple et moins chèreBon choix initial pour de nombreux SaaS, combiné aux filtres et au contexte de Lino.
Schema par TenantIsolation intermédiaireUtile lorsqu'il existe des exigences de séparation plus fortes sans aller jusqu'à une base de données par Tenant.
Base de données par TenantIsolation forteIndiquée lorsque le contrat, le volume ou l'exploitation justifient un coût et une automatisation plus élevés.

Pour la plupart des SaaS en phase initiale, une colonne par Tenant avec filtres globaux et contexte centralisé est un point de départ pragmatique. Lino aide avec cette base ; votre revue doit confirmer que les aggregates tenant-scoped portent le bon Tenant et que les nouvelles queries continuent d'utiliser le Tenant actuel validé par les abstractions générées.

Dans les services modulaires, l'isolation de Tenant et l'isolation de module sont des préoccupations différentes. Le module Catalog peut avoir une table de produits avec TenantId ; le module CRM peut avoir des clients avec TenantId ; et le module Tenancy reste propriétaire de l'enregistrement original des Tenants. La présence du même identifiant n'autorise pas l'accès direct entre modules.

Roles et permissions par Tenant

Dans un SaaS, un utilisateur peut être administrateur dans un Tenant et opérateur dans un autre. Lino génère la base pour travailler avec le lien utilisateur-Tenant, et les permissions des fonctionnalités doivent être évaluées dans le contexte actif validé par le framework.

Il existe aussi des permissions administratives hors Tenant. Un écran d'exploitation de la plateforme peut être accessible uniquement dans le contexte système, tandis que des pages comme produits et clients peuvent exiger un Tenant actif. Cette séparation indique clairement quand l'action est globale et quand elle dépend du Tenant résolu dans ITenantContext et de l'utilisateur authentifié dans IUserContext.

  • Modélisez les roles par Tenant lorsque les permissions varient par organisation.
  • Utilisez le contexte actuel dans les vérifications d'autorisation des pages, commands et queries.
  • Gardez les claims compactes, en portant dans le token uniquement ce qui doit participer au flux authentifié.
  • Révoquez ou mettez à jour les tokens lorsque des permissions critiques changent.
ContexteExempleTraitement
SystèmeAdministration de la plateforme, plans, Tenants et paramètres globaux.Exécuter hors du périmètre d'un Tenant spécifique.
TenantProduits, clients, commandes, utilisateurs et roles de ce Tenant.Utiliser le Tenant actuel validé par Lino.
Les deuxCertaines fonctions d'identité ou de gestion qui existent à la fois dans le système et dans le Tenant.Déclarer dans le cas d'utilisation quel contexte est utilisé.

JWT et sécurité de Tenant

Lorsque la fonctionnalité Tenant est active, Lino utilise le JWT comme source du contexte de l'utilisateur authentifié. Le IUserContext lit les claims de l'utilisateur et le TenantId du token, sans transformer le JWT en copie de la base de données.

  • Maintenez issuer, audience, signature et expiration correctement configurés.
  • Utilisez le Tenant validé par Lino au lieu d'accepter un changement manuel basé uniquement sur un header, une route ou le payload de l'écran.
  • Évitez les claims trop volumineux ; des tokens gonflés compliquent l'exploitation et le renouvellement.
  • Enregistrez des logs d'accès avec utilisateur, Tenant et opération lorsque le flux exige un audit.

Dans les applications avec sous-domaines, comme acme.dev.localhost et globex.dev.localhost, le ITenantContext représente le Tenant résolu pour la requête. Le IUserContext représente l'utilisateur authentifié et le Tenant présent dans le JWT. Lino compare ces deux contextes pour éviter qu'un token émis pour un Tenant soit utilisé dans un autre contexte de Tenant.

L'autorisation reste également contextuelle. La permission de voir les produits, par exemple, vaut dans le Tenant où elle a été accordée et pour lequel l'utilisateur a été authentifié. Lorsque vous créez de nouvelles fonctionnalités, utilisez les permissions et contextes déjà disponibles au lieu de créer des raccourcis qui reçoivent un TenantId isolé et ignorent le flux authentifié.

Une erreur non gérée est survenue. Rafraîchir 🗙