Analyze with AI
Get AI-powered insights from this Mad Devs article:
A B2B SaaS product can run for years with three roles: Admin, Member, and Viewer. Then enterprise customers ask for project administrators, custom roles, access to selected resources, delegated administration, organization hierarchies, and SSO group mapping. The team responds by adding roles and conditionals until nobody can answer a simple question quickly: Why can this user access this resource?
That is a reason to review the authorization architecture—not proof that RBAC must be replaced. The right goal is to identify which product rules no longer fit, preserve the parts that still work, and make the smallest change that keeps access decisions safe and explainable.
Key takeaways
- Basic RBAC is not a problem when stable roles still describe the product’s access rules.
- Role proliferation is usually a symptom. The underlying constraint may be resource scope, tenant variation, relationships, dynamic context, or duplicated checks.
- Tenant isolation and authorization support each other, but they solve different problems.
- Fine-grained authorization is useful only where broad roles cannot express the required decision.
- Scattered authorization logic creates consistency, auditability, and cross-tenant risks.
- Authorization modernization can proceed one bounded domain at a time; it does not require a backend rewrite.
Multi-tenant authorization determines whether a particular actor can perform an action on a resource in a tenant context. Tenant isolation prevents one tenant from accessing another tenant’s resources, even when infrastructure is shared. AWS describes tenant isolation as the explicit boundary that prevents one tenant from accessing another tenant’s resources; identity and authorization data alone do not create that boundary (AWS Security Blog).
The distinction changes how teams investigate an access-control problem. A permission such as project:update may be valid for Alice in Acme Corp and invalid for the same Alice in another organization. The decision therefore needs the actor, action, resource, and explicit tenant scope. Authentication alone cannot supply that complete answer.
This article focuses on how permissions evolve inside an existing SaaS product. For infrastructure and data-separation patterns, see the Mad Devs guide to cloud tenants and tenant isolation.
When basic RBAC is still enough
Keep basic RBAC when roles are stable, understandable to customers, and consistent across tenants. It remains a strong fit when permissions change infrequently, most decisions apply tenant-wide, and a small set of roles maps cleanly to job responsibilities.
Test that fit with real decisions rather than the number of customers. Take recent authorization requests from production or product specifications and express each as “role X may perform action Y within tenant Z.” If the rules remain clear without exceptions tied to specific objects or customers, a more expressive model may add operational cost without product value.
RBAC also does not require application code to check role names forever. Separating roles from permissions lets roles remain a customer-facing bundle while services enforce stable actions such as invoice:approve or member:invite. A new role can then reuse permissions without forcing changes across every endpoint.
Do not introduce fine-grained authorization simply because the product has grown. Introduce it when the decisions have genuinely become finer-grained.
Signs your SaaS is outgrowing basic RBAC
The strongest signal is not a long role list. It is the loss of a reliable mapping between a role and the decisions the product must make.
Role explosion
Admin becomes Billing Admin, Project Admin, Workspace Admin, Restricted Admin, and several customer-specific variants. Before replacing RBAC, inspect why each variant exists. If the variants only bundle stable permissions, cleaner role-to-permission mapping may solve the problem. If they encode different access to individual resources, the missing concept is probably scope.
Tenant-specific exceptions
The same role means different things for different customers, or enterprise contracts introduce exceptions that are implemented as tenant IDs in code. Ask whether those differences are legitimate configurable policy or accidental product forks. Configurable permission sets can handle the former; the latter may require product consolidation rather than a new authorization engine.
Resource-level access
A user can edit Project A, view Project B, and have no access to Project C. Tenant-wide roles cannot express this without multiplying roles or adding object-specific conditions. This is a clear case for resource-scoped assignments or relationships.
Users operate in multiple tenants
One identity may be an administrator in one organization and a viewer in another. Microsoft recommends explicitly assessing multi-tenant user access and designing tenant switching or consolidated views where needed (Azure Architecture Center). Never infer current tenant access from a user’s global identity or from their role in a different membership.
Delegated administration and identity-provider mapping
Enterprise customers may need their own administrators to assign access or map IdP groups to application roles. That changes who may manage policy, not only who may use a feature. Model delegation boundaries explicitly and test that a tenant administrator cannot grant privileges outside their tenant or beyond their own administrative authority.
Dynamic conditions
Access depends on resource ownership, workflow state, location, contract terms, or another attribute that changes independently of a role. Attribute-based rules may fit, but only after the attributes have trustworthy sources, defined update semantics, and clear failure behavior.
Different services implement slightly different versions of the same rule. A change requires tracing endpoints, background jobs, and service-to-service calls. AWS documents externalized authorization and centralized policy management as a way to decouple business logic from authorization logic (Amazon Verified Permissions). Centralization is worth considering here, even if the underlying model remains RBAC.
The following matrix is a Mad Devs editorial framework, not an industry standard. Use it to distinguish an untidy RBAC implementation from product requirements that genuinely need a more expressive model.
Run the assessment against evidence: support tickets, requested enterprise controls, current role definitions, access incidents, endpoint inventories, and code search results. A row in the right-hand column is not an automatic mandate for a policy platform. It identifies the constraint that the next architecture change must address.
For example, if only resource scope is problematic, add project-level grants in one domain. If the model is adequate but checks are duplicated, standardize enforcement. If tenant variation, relationships, and dynamic context all interact across services, centralized policy evaluation becomes a more credible option.
Modernization has several levels. Stop at the simplest one that makes decisions correct, maintainable, and explainable.
1. Clean up existing RBAC
Remove obsolete roles, document role intent, and eliminate role names that represent the same permission set. Verify the result by asking product, support, and engineering to explain each role consistently and by running existing allow-and-deny tests against the simplified mapping.
2. Separate roles from permissions
Let customers assign recognizable roles, but make application checks depend on permissions. Verify that a role can be renamed, split, or recomposed without changing endpoint logic. This often resolves role-management pain while retaining RBAC.
3. Add resource-scoped access
Introduce grants at the project, workspace, document, or other relevant boundary. Start with one resource type and define inheritance explicitly. Test parent-child behavior, moved resources, deleted memberships, and identical resource IDs in different tenants.
4. Add contextual or attribute-based rules
Use this level when access depends on properties beyond membership and role. Define the authority and freshness of every attribute. A policy that depends on stale location, ownership, or contract data is not reliably finer-grained—it is unpredictably wrong.
Consider a policy decision point (PDP), with policy enforcement points (PEPs) at APIs or other protected boundaries, when many services need the same complex decisions. AWS Verified Permissions is one example of an externalized policy decision service; the application still enforces the returned decision at the protected operation (Amazon Verified Permissions). Evaluate latency, availability, policy deployment, caching, observability, and failure mode before selecting OPA, Cedar, a managed service, or another engine.
The evolution can be summarized as simple RBAC → roles plus permissions → resource scope → contextual rules → centralized policy decisions. This is a menu, not a maturity ladder. A product should stop wherever its real requirements are met.
A mature SaaS product may use more than one model. An organization-wide Billing Admin role can coexist with project membership relationships and a contextual approval-limit rule. The appropriate combination depends on the decisions the product must express, rather than a universal hierarchy of authorization models (Auth0).
Choose at the decision level. Write representative rules in plain language, identify which inputs they require, and check whether engineers and customer administrators can predict the result. The goal is not to label the whole product with one acronym; it is to keep each decision understandable.
Keep tenant context explicit
Tenant context should be a first-class input to every authorization decision, not an implicit property inferred wherever convenient. Define an authoritative source for the active tenant, validate the actor’s membership, and confirm that the resource belongs to that tenant before evaluating action-specific permission.
Review the complete request lifecycle:
- tenant selection and switching;
- resource loading and ownership checks;
- public and internal API calls;
- service-to-service identity and propagation;
- asynchronous jobs that run without an interactive session;
- cache keys and cached authorization decisions;
- support and administrative access.
A practical negative test uses a valid identity, a valid resource identifier, and the wrong tenant context. The request must fail even though every component looks valid in isolation. Repeat that test at APIs, job handlers, and service boundaries rather than only in the UI.
Separate product entitlements from user permissions
An entitlement answers what a customer account has purchased or been granted. Authorization answers what a specific actor may do with a specific resource.
Entitlement: Acme Corp has access to advanced reporting.
Authorization: Alice from Acme may export this report.
If a pricing plan becomes the permission system, enabling a feature for a tenant can accidentally imply that every member may perform every related action. Auth0 makes the same distinction: entitlements define granted capabilities, while authorization enforces what a user can do in context (Auth0).
Keep the two decisions composable and observable. First confirm that the tenant has the feature; then evaluate the user’s permission within the tenant and resource scope. Test both failure paths independently so support teams can explain whether access was denied because of the account’s plan or the user’s authority.
List protected actions and where they are enforced: endpoints, services, job workers, database access layers, and administrative tools. Record the actor, tenant, action, resource, inputs, and current allow/deny behavior.
2. Identify the actual complexity
Group failures by cause: role design, resource scope, tenant variation, relationships, context, delegation, or duplicated code. Use the assessment matrix to avoid selecting technology before the constraint is clear.
3. Define the target model in product language
Write rules such as “a project administrator may archive projects they administer within the active organization.” Resolve ownership, inheritance, tenant boundaries, and default-deny behavior before translating rules into RBAC, ABAC, ReBAC, or hybrid policies.
4. Introduce a stable enforcement boundary
Choose one API, domain, or resource type with meaningful complexity and bounded blast radius. Standardize the request to include actor, tenant, action, resource, and necessary context. Keep enforcement close to the protected operation even if decisions are evaluated elsewhere.
5. Compare old and new decisions safely
Where feasible, run the new evaluator in shadow mode. Log disagreements without letting the new result grant access. Investigate both “old allow/new deny” and “old deny/new allow”; the second case may reveal a dangerous expansion.
6. Move incrementally
Migrate one decision family at a time, with rollback criteria and deny-path tests. Preserve broad RBAC for the majority of rules if it still works. Mad Devs applies the same constraint-first approach to backend modernization and architecture improvement: change the parts that limit the product rather than treating a full rewrite as the starting point.
7. Remove obsolete logic
After validation, delete superseded checks and document the new source of truth. Leaving parallel authorization paths indefinitely makes future decisions harder to explain and can reintroduce inconsistent enforcement.
Authorization tests need adversarial tenant and permission combinations, not only successful requests. Cover at least:
- an allowed action in the correct tenant;
- a denied action in the correct tenant;
- a valid resource ID submitted under the wrong tenant;
- one user with different roles in different tenants;
- tenant switching and stale sessions;
- revoked membership or permission;
- custom roles and delegated administration limits;
- resource ownership and parent changes;
- background and service-to-service requests;
- cache invalidation after access changes.
Test deny behavior at every enforcement boundary and confirm it fails closed when tenant context, attributes, or the policy service are unavailable. Auth0 likewise recommends testing allow and deny paths as part of scaling fine-grained access control (Auth0). Logs should explain the decision without exposing sensitive policy data: actor, active tenant, action, resource reference, decision, policy version, and a bounded reason code are usually more useful than a raw policy dump.
- Can the team explain every role and its intended scope?
- Are permissions separated from customer-facing role names?
- Do tenants genuinely need different permission sets?
- Can one user belong to multiple tenants with different access?
- Are resource-level permissions required?
- Are rules duplicated across services or endpoints?
- Can a developer explain why a request was allowed?
- Are decisions logged where operationally and legally appropriate?
- Are tenant boundaries and deny paths tested automatically?
- May customer administrators manage permissions, and within what limits?
- Are SSO or IdP groups mapped to roles with clear precedence?
- Are product entitlements separate from user permissions?
- Can the model absorb a new requirement without another exception role?
- Does the remaining problem justify fine-grained authorization or a policy engine?
Frequently asked questions
Should we replace RBAC when enterprise customers request custom roles?
Do we need ABAC or relationship-based authorization for every multi-tenant product?
How can we change authorization without exposing a tenant boundary?
The desired outcome is not the most sophisticated authorization architecture. It is the simplest model that can express current product rules, enforce tenant boundaries consistently, and evolve without turning each enterprise request into a cross-tenant risk.
BACKEND ARCHITECTURE & MODERNIZATION Has your authorization model become harder to change than the product itself? Mad Devs can review your current tenant, role, permission, and backend boundaries, identify where authorization complexity actually comes from, and define a focused modernization path without defaulting to a full rewrite. Discuss your backend → |






