RBAC Design in SaaS Applications
How to design tenant-scoped roles, permission models, and policy enforcement to prevent broken access control in SaaS systems.
RBAC Design in SaaS Applications
Most SaaS authorization failures do not originate from missing authentication. They originate from poorly designed authorization boundaries.
Systems authenticate users successfully and then proceed to grant excessive authority across tenants, modules, or data domains. The result is broken access control. In multi tenant SaaS systems this class of failure often leads to cross organization data exposure or privilege escalation.
Role based access control is commonly presented as a simple concept. In practice it becomes one of the most structurally important layers of SaaS architecture. It defines how authority propagates through the application, how actions are constrained by tenant context, and how sensitive operations are protected.
This article focuses on implementation level RBAC design for multi tenant SaaS systems built with ASP.NET Core and relational databases.
If you’re building a SaaS product, this is exactly the kind of issue that appears when tenant boundaries are not designed properly. Teams that need SaaS development for multi-tenant systems usually define roles, permissions, and data ownership together.
RBAC design fails without tenant isolation
If role checks pass but cross-tenant reads still happen, this is exactly the authorization gap our SaaS security audit uncovers.
For system-level context, pair this with SaaS Security Architecture: A Practical Engineering Guide and Organization-Level Data Isolation in Multi-Tenant SaaS.
Authorization Boundary in Multi Tenant SaaS
In a multi tenant system the authorization boundary is not simply user identity. It is the combination of identity and tenant context.
A request entering the system carries several pieces of information:
- authenticated user identity
- tenant identifier
- role assignments within that tenant
- action being attempted
Authorization decisions must incorporate all four.
This is not just an implementation detail. This is a system design problem.
If you’re building a SaaS product, this is the level where architecture decisions start affecting security, cost, and product behavior. SaaS development services are relevant when authorization has to remain tenant-scoped across the request pipeline.
Typical request pipeline:
HTTP Request
->
Authentication
->
Tenant Resolution Middleware
->
Tenant Context Injection
->
Authorization Policy Evaluation
->
Application Logic
Tenant resolution and authorization are tightly coupled.
Roles vs Permissions
RBAC systems combine two concepts.
Roles represent organizational responsibility.
Examples:
- Owner
- Administrator
- Manager
- Analyst
- Viewer
Permissions represent specific capabilities.
Examples:
- users.read
- users.create
- billing.update
- auditlog.export
- tenant.settings.update
Roles map to collections of permissions.
Roles should rarely be hardcoded in application logic.
Example of problematic logic:
if(user.Role == "Admin")
{
// allow operation
}Permissions provide a more stable abstraction for authorization policies.
Tenant Scoped Authorization
A defining property of SaaS RBAC systems is tenant scope. This is also the core boundary tested in a SaaS security audit for cross-tenant leaks.
A user may hold different roles in different organizations.
Example:
User: alice@example.com
Tenant A -> Administrator
Tenant B -> Viewer
The database must model role membership as:
User + Tenant + Role
Example schema:
Users Id
Email
PasswordHash
Tenants Id
Name
Roles Id
Name
Permissions Id
Key
RolePermissions RoleId
PermissionId
UserTenantRoles UserId
TenantId
RoleId
This model prevents global role assignments across tenants.
Modeling RBAC in a Relational Database
Authorization checks occur frequently so relational modeling must support efficient queries.
Example query used to evaluate permissions:
SELECT p.Key
FROM Permissions p
JOIN RolePermissions rp ON rp.PermissionId = p.Id
JOIN UserTenantRoles utr ON utr.RoleId = rp.RoleId
WHERE utr.UserId = @UserId
AND utr.TenantId = @TenantIdRecommended indexes:
- UserTenantRoles(UserId, TenantId)
- RolePermissions(RoleId)
- Permissions(Key)
High scale systems often cache effective permissions in memory.
Cache keys must include tenant identifiers.
Authorization in ASP.NET Core
ASP.NET Core provides a policy based authorization framework.
Example policy definition:
services.AddAuthorization(options =>
{
options.AddPolicy("UsersRead", policy =>
policy.Requirements.Add(new PermissionRequirement("users.read")));
options.AddPolicy("UsersCreate", policy =>
policy.Requirements.Add(new PermissionRequirement("users.create")));
});Permission requirement:
public class PermissionRequirement : IAuthorizationRequirement
{
public string Permission { get; }
public PermissionRequirement(string permission)
{
Permission = permission;
}
}Authorization handler:
public class PermissionHandler : AuthorizationHandler<PermissionRequirement>
{
private readonly IPermissionService _permissions;
public PermissionHandler(IPermissionService permissions)
{
_permissions = permissions;
}
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context,
PermissionRequirement requirement)
{
var userId = context.User.GetUserId();
var tenantId = context.User.GetTenantId();
if (await _permissions.UserHasPermission(userId, tenantId, requirement.Permission))
{
context.Succeed(requirement);
}
}
}Controllers enforce policies.
[Authorize(Policy = "UsersCreate")]
[HttpPost]
public async Task<IActionResult> CreateUser(CreateUserRequest request)
{
...
}Preventing Privilege Escalation
Privilege escalation often occurs through administrative endpoints.
Common vectors:
- role editing APIs
- tenant settings updates
- user invitation flows
- role assignment endpoints
Example vulnerability:
POST /api/users/{id}/role
If administrators can assign any role including Owner they can escalate privileges.
Safer implementation validates role hierarchy.
Owner
Administrator
Manager
Viewer
Users may only assign roles lower than their own.
Example validation:
if(targetRole.Level >= currentUserRole.Level)
{
return Forbid();
}Broken Access Control Failure Scenario
Example endpoint:
var project = await _db.Projects
.FirstOrDefaultAsync(p => p.Id == projectId);The query ignores tenant boundaries.
If project identifiers are guessable a user from Tenant A may retrieve projects belonging to Tenant B.
Correct query:
var project = await _db.Projects
.Where(p => p.TenantId == tenantId)
.FirstOrDefaultAsync(p => p.Id == projectId);Even better is enforcing isolation using EF Core global query filters.
modelBuilder.Entity<Project>()
.HasQueryFilter(p => p.TenantId == _tenantContext.TenantId);RBAC alone cannot prevent this class of failure, which is why teams pair permission design with an external review before incidents.
The remaining risk is usually drift between role policy, tenant scope, and the query that actually executes.
This is where an authorization testing tool is useful: it checks whether role rules still hold when the same object is replayed under different actors and tenant contexts. Teams that need a manual review of policy enforcement and live request paths usually start with an API authorization audit for SaaS systems.
We validate RBAC plus object-level enforcement
The audit verifies that tenant scope and permission policies actually hold in data access, not only in controller attributes.
Authorization and Data Isolation
RBAC and tenant isolation must reinforce each other.
Tenant isolation protects data boundaries.
RBAC protects action boundaries.
Typical evaluation sequence:
Authentication
Tenant Resolution
Tenant Data Isolation
Authorization Policy
Application Logic
Authorization policies must never execute without tenant context.
Policy Composition
Complex SaaS systems require composed authorization policies.
Example:
options.AddPolicy("EditDocument", policy =>
{
policy.Requirements.Add(new PermissionRequirement("documents.edit"));
policy.Requirements.Add(new DocumentOwnerRequirement());
});Each requirement is evaluated independently.
This keeps controllers free from complex authorization logic.
Operational Considerations
RBAC systems must remain observable and auditable.
Recommended log fields:
- UserId
- TenantId
- Permission evaluated
- Resource identifier
- Request path
Audit logs should record:
- role assignment
- permission changes
- tenant ownership transfer
Without these logs security incidents are difficult to investigate.
Engineering Checklist
A minimal RBAC implementation should ensure:
- roles are scoped to tenants
- permissions represent system capabilities
- authorization policies rely on permissions
- role assignment endpoints enforce hierarchy
- database queries enforce tenant isolation
- authorization decisions are logged
- permission caching includes tenant context
Closing Thoughts
RBAC is structural infrastructure inside a SaaS system.
Weak RBAC designs eventually manifest as broken access control and cross organization data exposure.
Strong implementations combine:
- tenant scoped identity
- permission driven authorization
- strict tenant data isolation
ASP.NET Core provides the primitives to build this model but the architecture must be designed intentionally.
If you’re building or planning a SaaS product, we design systems where this class of issue does not happen. SaaS development for multi-tenant systems is most useful when RBAC still needs to be shaped around the product model.
Need implementation support? Review the test API access control or book a SaaS security audit.
Need external review of RBAC and tenant boundaries?
We identify authorization failures, BOLA paths, and cross-tenant exposure patterns in ASP.NET Core SaaS applications.
Related Articles
Continue reading in SaaS Security
Building SaaS with complex authorization?
Move from theory to request-level validation and architecture decisions that hold under scale.
SaaS Security Cluster
This article is part of our SaaS Security Architecture series.
Start with the pillar article: SaaS Security Architecture: A Practical Engineering Guide
