FullStack Course LogoFullStack Course
Module: Full Stack
Full Stack·152·11 MIN READ

152: Authorization: RBAC, ABAC, Ownership, and Multi-Tenant Isolation

TOPICS COVERED: Authorization: RBAC, ABAC, Ownership, and Multi-Tenant Isolation

Learning outcomes

By the end of this lesson, you can:

  • explain and apply RBAC in a realistic implementation;
  • explain and apply ABAC in a realistic implementation;
  • explain and apply ownership checks in a realistic implementation;
  • explain and apply tenant isolation in a realistic implementation;
  • explain and apply state-dependent permissions in a realistic implementation.

Prerequisites and retrieval

This lesson assumes the earlier 01–06 foundation and the preceding lessons in this module. Before you read, retrieve one concrete example from an earlier project in which this concern appeared. Perhaps the application had different staff roles, records that belonged to users, or data that had to remain inside a customer account. The point is not to memorize a list of terms. It is to make and defend an authorization decision in a production full-stack web application with a React client, a Node.js API, persistent storage, authentication, observability, and deployment concerns.

Terminology

  • RBAC: Role-based access control groups permissions into operational roles. A role is useful when the same set of capabilities applies to a meaningful class of users.
  • ABAC: Attribute-based access control evaluates subject, resource, action, and environment attributes. The decision can therefore depend on context, not only on a named role.
  • Ownership checks: “User can update own record” is a resource predicate, not just a role. The server must establish which resource is being accessed and who owns it.
  • Tenant isolation: Every tenant-owned query must be scoped by tenant identity at a trusted server boundary. A user from one tenant must not be able to obtain another tenant's data by changing an identifier or request parameter.
  • State-dependent permissions: A permission may change with workflow state: a draft order can be edited, while a paid invoice may require a reversal workflow instead of an ordinary update.
  • Policy testing: Build table-driven tests for both allow and deny cases. The denied cases are part of the policy contract, not optional extra coverage.

Mental model

Treat Authorization: RBAC, ABAC, Ownership, and Multi-Tenant Isolation as a design problem with observable inputs, outputs, invariants, and failure modes. Authorization answers a specific question: may this authenticated principal perform this operation on this particular resource? That decision must be enforced on the server for every protected path, even when the client hides the button or disables the form.

A strong implementation makes its assumptions visible, narrows uncertainty at system boundaries, and leaves enough evidence to explain why the design is safe. That evidence may be tests, types, database constraints, metrics, or diagrams. When a policy fails, those artifacts should help you identify whether the mistake is in input parsing, identity, resource lookup, policy evaluation, or persistence.

A useful sequence for both production design and interviews is:

text
requirement -> constraints -> model -> implementation -> failure analysis -> verification

Do not jump from a requirement directly to a library call. First state what must remain true. Then select the mechanism that enforces that invariant and decide how you will verify it. For example, “a manager may approve an invoice” is incomplete until you specify the tenant, invoice state, and any ownership or amount constraints that are part of the rule.

Deep dive

1. RBAC

Role-based access control groups permissions into operational roles. It is easy to explain and often a good starting point: a cashier, manager, and auditor can each receive a named set of capabilities. RBAC becomes brittle, however, when the real rule also depends on branch, tenant, ownership, amount, state, or time. Adding more and more roles to represent those dimensions can hide the actual policy and produce a difficult-to-maintain role matrix.

Decision rule: Use RBAC deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer a more explicit design.

2. ABAC

Attribute-based access control evaluates attributes of the subject, resource, action, and environment. That makes it suitable for contextual policy, such as allowing an operation only for a user's tenant, on an invoice in a particular state, during an approved workflow, or below a relevant amount. The flexibility has a cost: attributes must be modeled consistently, loaded reliably, and covered by a test matrix.

Decision rule: Use ABAC deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer a more explicit design.

3. Ownership checks

“User can update own record” is a resource predicate, not merely a role check. A user may have the right role and still be forbidden from changing a record owned by somebody else. Fetch or scope the resource in a way that binds the ownership and tenant checks to the lookup; do not trust a client-supplied identifier to establish access.

This distinction matters because checking ownership after an unrestricted lookup can leak information, and checking only the identifier in application code can leave another query path unprotected. The resource lookup and the authorization decision should agree about which record is being acted on.

Decision rule: Use ownership checks deliberately when they make the contract or invariant easier to prove. If a shortcut only reduces typing while hiding an assumption, prefer the more explicit design.

4. Tenant isolation

Every query for tenant-owned data must be scoped by tenant identity at a trusted server boundary. The tenant context should come from authenticated server-side identity or another trusted source, not from an arbitrary client field. A database row-level security policy can provide defense in depth, but it does not replace correct application context: the application still has to establish the right tenant and use the right transaction or database session context.

Tenant isolation applies to reads, writes, updates, deletes, background jobs, exports, and administrative paths. A list endpoint that is correctly scoped does not make a separate “get by ID” endpoint safe automatically.

Decision rule: Use tenant isolation deliberately when it makes the contract or invariant easier to prove. If a shortcut only reduces typing while hiding an assumption, prefer the more explicit design.

5. State-dependent permissions

A permission can change as a workflow changes state. A draft order may be editable, while a paid invoice may require a reversal workflow rather than a direct edit. Authorization therefore needs to understand both the requested operation and the permitted state transition. A generic “has update permission” check is not enough if it allows an invalid transition.

State checks also need to account for concurrency. The state observed during authorization must not silently become stale before the write. The persistence operation or transaction should enforce the consistency expectation appropriate to the workflow.

Decision rule: Use state-dependent permissions deliberately when they make the contract or invariant easier to prove. If a shortcut only reduces typing while hiding an assumption, prefer the more explicit design.

6. Policy testing

Build table-driven tests for allow and deny cases. Include combinations of role, tenant, ownership, action, and resource state rather than testing each attribute in isolation. Negative tests are essential because an authorization defect is often invisible on the normal happy path: the expected user can still perform the expected action, while an unintended user can do the same thing through a modified request.

Decision rule: Use policy testing deliberately when it makes the contract or invariant easier to prove. If a shortcut only reduces typing while hiding an assumption, prefer the more explicit design.

Worked example

Consider a production full-stack web application with a React client, a Node.js API, persistent storage, authentication, observability, and deployment concerns. Start by writing the requirement in one sentence. Then list the input and output contracts and identify which concept above owns each failure mode. For an invoice operation, that might mean separating the user's role from the invoice's tenant, the invoice's owner, and its current state.

The important move is separation. Parsing and validation belong at the boundary. Domain rules belong in the domain or service layer. Persistence rules belong in the database or repository. Presentation rules belong in the client. Mixing these concerns can make a happy-path demo look shorter, but it makes edge cases and authorization bypasses much harder to reason about.

ts
export async function handleRequest(input: unknown) {
  const command = parseCommand(input);
  const result = await service.execute(command);
  return toHttpResponse(result);
}

This handler sketch is intentionally small. parseCommand should establish the shape and basic validity of untrusted input; service.execute should apply the authenticated principal, resource, tenant, and state rules; and toHttpResponse should translate the result into the API contract. The exact division may vary, but the authorization decision must not be left to the React client.

Walk the example through at least four cases: the normal path, an empty or missing value, a duplicate/retry/concurrent path where relevant, and a dependency failure. For each case, state which layer detects the problem and what the caller observes. For example, malformed input should be rejected at the boundary, a cross-tenant resource should not pass the service policy, and a database or dependency failure should produce the documented error behavior rather than an accidental success. This is the level of explanation expected in a senior code review or technical interview.

Production perspective

Production correctness is broader than “the code works on my machine.” Ask how the design behaves during deploys, retries, partial failure, stale clients, concurrent requests, malformed data, schema changes, and high-cardinality workloads. Prefer explicit contracts, bounded resource usage, structured errors, and measurable behavior. Optimize only after you can identify the bottleneck or risk with evidence.

When the topic involves an external dependency, define a timeout and cancellation strategy. When it involves persistence, define transaction and consistency expectations. When it involves user-visible state, define loading, empty, error, stale, and success states. When it involves security, assume that the client can be modified and that network input is untrusted. A hidden client-side control is not an authorization boundary.

Guided lab

Model cashier, manager, and auditor access to invoices across two tenants. Implement one policy function and a test matrix covering role, tenant, ownership, and invoice state. Make the matrix explicit enough to show both permitted and rejected combinations; a single successful example cannot demonstrate tenant isolation.

Complete the lab with this discipline:

  1. Write the requirement and two non-requirements.
  2. List input, output, and error contracts before implementation.
  3. Implement the smallest correct vertical slice.
  4. Add at least one invalid-input test and one edge-case test.
  5. Instrument or inspect the behavior instead of guessing.
  6. Refactor one hidden assumption into an explicit type, constraint, function, or configuration.
  7. Explain one alternative design and why you did not choose it.
  8. Record a short “what would break at 10× scale?” note.

Edge cases and failure modes

  • RBAC: Test absence, malformed input, duplicates, ordering or concurrency where applicable, and behavior at the smallest and largest credible sizes.
  • ABAC: Test absence, malformed input, duplicates, ordering or concurrency where applicable, and behavior at the smallest and largest credible sizes. In particular, verify what happens when an attribute cannot be loaded.
  • Ownership checks: Test absence, malformed input, duplicates, ordering or concurrency where applicable, and behavior at the smallest and largest credible sizes. Include a resource identifier that belongs to another user.
  • Tenant isolation: Test absence, malformed input, duplicates, ordering or concurrency where applicable, and behavior at the smallest and largest credible sizes. Include direct lookup, list, update, delete, and background-work paths where they exist.
  • State-dependent permissions: Test absence, malformed input, duplicates, ordering or concurrency where applicable, and behavior at the smallest and largest credible sizes. Include stale state and invalid transitions.

Common mistakes and debugging

  • Solving the example instead of the requirement: a copied pattern can be syntactically correct but architecturally wrong.
  • Hiding uncertainty with assertions, broad exception handlers, permissive schemas, or “temporary” any values.
  • Testing only the happy path and therefore discovering contracts only after integration.
  • Optimizing before measuring, or selecting a scalable mechanism without a scale requirement.
  • Letting client-side behavior stand in for server-side authorization, validation, or persistence guarantees.

For debugging, reproduce the smallest failing case and inspect the actual value or execution plan. Trace the boundary where the invariant first becomes false: source or build, browser or DOM, Network or HTTP, server or route, database or query, or deployment or configuration. Then fix the owning layer instead of adding a downstream patch. For an authorization failure, log enough structured context to investigate the decision without exposing tokens, credentials, or unnecessary sensitive data.

Interview questions

  1. What problem does RBAC solve, and what trade-off or failure mode would make you choose a different approach?
  2. What problem does ABAC solve, and what trade-off or failure mode would make you choose a different approach?
  3. What problem do ownership checks solve, and what trade-off or failure mode would make you choose a different approach?
  4. What problem does tenant isolation solve, and what trade-off or failure mode would make you choose a different approach?
  5. What problem do state-dependent permissions solve, and what trade-off or failure mode would make you choose a different approach?

Checkpoint

Without notes, explain Authorization: RBAC, ABAC, Ownership, and Multi-Tenant Isolation to another developer in five minutes. Your explanation must include one invariant, one edge case, one production failure mode, and one alternative design. Then implement a small example without copying the lesson code. Be able to say where the server obtains identity and tenant context, which layer evaluates the policy, and how the persistence operation prevents a race or cross-tenant access.

Mastery checklist

  • I can define the core terms precisely.
  • I can choose a design from requirements instead of from habit.
  • I can implement and test the normal path and edge cases.
  • I can explain the runtime, storage, or complexity cost.
  • I can identify which layer owns validation, errors, and recovery.
  • I can compare at least two reasonable alternatives.
  • I can explain how the design changes at larger scale or stricter reliability.

References

Reader page: /fullstack/lesson/152/authorization-rbac-abac-ownership-and-multi-tenant-isolation