SF

Multi-tenant · EF Core · JWT

Isolation multi-tenant : isoler sans ralentir l'API

Comment garantir qu'aucune requête ne peut franchir une frontière de tenant, sans ajouter un filtre à chaque appel.

5
agrégats couverts
3
rôles RBAC
0
filtre manuel par requête

Le problème

Garantir qu'aucune requête ne peut franchir une frontière de tenant — sans faire reposer cette garantie sur la discipline de chaque développeur à ajouter un filtre à chaque appel.

L'approche

Le tenant courant est résolu depuis un claim JWT dès l'authentification, puis injecté dans un Global Query Filter EF Core configuré une seule fois au niveau du DbContext. Chaque requête LINQ, même écrite sans y penser, est automatiquement restreinte au tenant actif. Par-dessus, un RBAC entity-level distingue Admin, Manager et Collaborateur pour les permissions qui ne dépendent pas du tenant.

Extrait de codeApplicationDbContext.cs
// Tenant isolation is enforced once, in the model — not in every query.
// A developer who forgets a WHERE clause cannot leak another tenant's rows,
// because the filter is compiled into every LINQ query EF Core generates.
protected override void OnModelCreating(ModelBuilder builder)
{
    foreach (var entity in builder.Model.GetEntityTypes()
                 .Where(e => typeof(ITenantScoped).IsAssignableFrom(e.ClrType)))
    {
        builder.Entity(entity.ClrType)
               .HasQueryFilter(BuildTenantFilter(entity.ClrType));

        // Composite index: every filtered query starts with TenantId,
        // so it must lead the index or the filter costs a scan.
        builder.Entity(entity.ClrType)
               .HasIndex(nameof(ITenantScoped.TenantId), "Id");
    }
}

// TenantId comes from the validated JWT, never from the request body.
private LambdaExpression BuildTenantFilter(Type clrType)
{
    var parameter = Expression.Parameter(clrType, "e");
    var property = Expression.Property(parameter, nameof(ITenantScoped.TenantId));
    var current = Expression.Property(
        Expression.Constant(_tenantContext), nameof(ITenantContext.TenantId));

    return Expression.Lambda(Expression.Equal(property, current), parameter);
}

Le résultat

L'isolation devient une propriété du framework, pas une convention à respecter. Un nouveau endpoint hérite du filtre sans rien écrire de spécifique : l'erreur la plus commune en multi-tenant, l'oubli du filtre sur une requête isolée, disparaît structurellement.

PostgreSQL · SignalR · TestcontainersConcurrence optimiste : le conflit qu'on ne voit pasASP.NET Core Identity · JWT · SécuritéLa session qu'on croyait fermée