06 · Multi-tenancy · 2022-now · Work

Multi-tenant Access Control

Tenant isolation enforced by scoped access keys and an EF Core global query filter applied to protected entities, so isolation does not depend on every query remembering its WHERE clause.

Any queryNO WHERE CLAUSEBRANCH A ACCESS KEYTENANT FILTERBRANCH ABRANCH B×BRANCH C×BRANCH ABRANCH C×BRANCH B×BRANCH AISOLATION AT THE ORM · NOT IN EVERY QUERY

The problem

The app serves multiple tenants off shared infrastructure, so a single missed filter could surface one tenant inside another tenant results. That is not a risk worth resting on every developer remembering to write the same clause correctly every time.

What was built

Tenant isolation through scoped access keys, combined with an EF Core global query filter applied to all protected entities.

◆Scoping happens at the ORM level, so a query that forgets to filter explicitly cannot quietly bypass it.
◆Access keys map to existing branches, and any user with access to a branch carries that branch key.
◆Since joining I have extended it: bringing new entities under protection as secured data came online, adding access groups to the SignalR hubs, and maintaining the surrounding code.
◆Result: a class of data-leak risk that used to depend on developer diligence is now enforced structurally instead.

What I'd do differently

I would use a separate named query filter rather than the single global one. When this was built, .NET Core supported only one query filter, so ignoring a filter meant ignoring all of them at once. Named filters in newer versions would let us selectively ignore other filters without ever touching tenant protection - something I have been scoping out as we move to a newer .NET version.