Authorization decision flow from a server session through platform role, active membership, active organization, advisor-client assignment, and relationship-scoped resource to an allow-or-deny result.
A role is only one input. The server resolves active tenant and business relationships before allowing an action.

Authorization Is More Than a Role

Designing server-authoritative, relationship-scoped access in a multi-tenant application

Abstract

Role-based access control is useful until the business question becomes more specific than “What kind of user is this?”

In an advisor-managed application, an advisor role does not establish access to every client. The system must also know whether the advisor belongs to an active organization, whether the client belongs to the same organization, whether an active assignment connects them, whether the target resource belongs to that relationship, whether the requested action is permitted, and whether any required entitlement or step-up proof remains valid.

This paper describes an authorization model built around server-confirmed attributes and relationships rather than a role alone. It covers the data model, request-time resolver, database constraints, denial behavior, audit context, and negative tests needed to make a shared-database tenant boundary credible.

1. The role-shaped trap

The first version of many internal tools has a guard like this:

if user.role is advisor:

allow

It works while every advisor is effectively trusted with the same population. Then the product becomes multi-tenant, or advisors receive explicit client assignments, or support needs read-only inspection without mutation rights.

The role has not become useless. It has become insufficient.

The actual authorization question is closer to:

allow =

authenticated session

AND platform role permits the action family

AND organization membership is active

AND organization is active

AND membership role matches the workspace

AND target belongs to the same organization

AND active relationship connects actor and target

AND resource belongs to that relationship

AND entitlement permits the capability

AND step-up proof is present when required

That is not conventional role lookup. It is an attribute- and relationship-based decision.

OWASP recommends denying by default, validating permission on every request, and preferring attribute- or relationship-based access control when simple RBAC cannot express the business context.1 Those recommendations sound obvious. The hard part is making them the path of least resistance for every route.

2. Separate the identities the business separates

The authorization model in this case study uses three distinct concepts:

  1. Platform role — coarse access such as consumer, internal advisor, support, or administrator.
  2. Organization membership — who belongs to a tenant and in what capacity.
  3. Advisor-client assignment — which advisor is responsible for which client.

Membership answers:

Is this user part of the organization?

Assignment answers:

Is this advisor responsible for this client right now?

Conflating them creates either excessive access or an awkward schema. If every advisor can see every client in the tenant, assignment becomes decorative. If assignment is encoded as a special membership role, historical transfers and multiple relationship types become difficult to represent.

The model also keeps the client as the subject of financial plans while preserving the advisor or administrator as the actor. That distinction prevents user_id from changing meaning depending on which workflow wrote the row.

3. Put invariants in the database

Application checks are necessary, but some rules are important enough to have a second enforcement layer.

Two examples from the schema are:

  • one active organization membership per user;
  • one active advisor assignment per client within an organization.

Both are represented with partial unique indexes. PostgreSQL partial indexes can enforce uniqueness only for rows matching a predicate, which is a useful fit for historical membership models: many ended rows may exist, while only one active row is permitted.2

Conceptually:

CREATE UNIQUE INDEX one_active_membership

ON organization_memberships (user_id)

WHERE status = 'active';

CREATE UNIQUE INDEX one_active_assignment

ON advisor_client_assignments (organization_id, client_user_id)

WHERE status = 'active';

The database is not replacing authorization. It is preventing the application from creating contradictory authority.

That difference matters. A tenant filter answers whether a request may read a row. A uniqueness constraint prevents the system from entering an ambiguous state in which two organizations or advisors both appear current.

4. Resolve context from trusted state

The browser can select a client, show a tenant logo, or include an organization ID in a URL. None of those facts grant access.

The server reconstructs context from the authenticated session and stored relationships:

Authorization context flow: Server session → platform role → active membership → active organization → active advisor-client assignment → relationship-scoped resource → allow or deny action.

A request parameter identifies the target to evaluate. It does not establish the actor’s right to that target.

This leads to a practical resolver pattern:

  • resolve the active membership for the actor;
  • verify the organization is active;
  • verify the membership role required by the workspace;
  • load only active assignments in that organization;
  • for a target client, verify active client membership in the same organization;
  • load the current assignment using both organization and client;
  • verify the assignment names the requesting advisor;
  • return a typed context object that downstream handlers must use.

The returned context carries the organization and assignment identifiers needed for queries, writes, notifications, and audit events. Routes should not repeat the logic or rebuild part of it from request data.

5. Make unauthorized resources disappear

Authorization failures can disclose information.

If an advisor requests a client who exists in another tenant and receives:

403: You do not have access to Client Jane Doe

the system has confirmed that the client exists.

For relationship-scoped reads, the case-study implementation returns a not-found response for unassigned and cross-tenant targets. The authenticated advisor can distinguish “my assigned client” from “not visible to me,” but cannot use the endpoint as a directory of global identities.

This is not a universal rule. Support and administrative workflows may need different error semantics. The point is to choose denial behavior as part of the threat model instead of accepting whatever error falls out of a generic middleware function.

6. Support access is not advisor access

Internal roles become dangerous when they are treated as a single ladder in every workflow.

Support may need to inspect:

  • recent authentication problems;
  • connection health;
  • organization and assignment context;
  • billing state;
  • bounded recovery guidance;
  • request identifiers and timelines.

That does not imply permission to change a routing plan.

The model therefore separates:

  • advisor access through an active client assignment;
  • support-safe read access for investigation;
  • administrative mutation access;
  • organization administration for tenant setup.

The audit record preserves the inspection scope so a later reviewer can tell whether data was viewed through an advisor relationship or an internal support function.

Least privilege becomes more credible when the product has a useful read-only path. If the only operational tool is an all-powerful admin console, routine support work will eventually use it.

7. Entitlement is another authorization input

Authentication answers who the user is. Tenant context answers where the user is operating. Assignment answers through which relationship. Entitlement answers whether the organization currently has the capability.

These decisions should remain separate even if they are evaluated together.

An organization-sponsored client may have:

  • active membership;
  • active advisor assignment;
  • an inactive or expired organization entitlement.

The relationship is still real, but the paid capability may be unavailable. Deleting or ignoring the relationship because billing changed would corrupt history. Conversely, allowing access because the relationship exists would bypass the commercial control.

Keeping entitlement as a named decision with explicit reasons—missing, inactive, suspended, expired, or exhausted—makes both enforcement and support clearer.

8. Carry context into every consequential write

Authorization context should survive the request.

Relationship-scoped records include:

  • organization ID;
  • assignment ID;
  • client or subject ID;
  • advisor or actor ID;
  • creator ID where authorship matters.

Audit events add:

  • actor role;
  • action type;
  • target type and ID;
  • changed fields;
  • request and session identifiers;
  • network and user-agent context where appropriate.

This is not gratuitous denormalization. It prevents history from depending on current membership. If a client transfers to another organization, old plan, message, and audit records should not appear to have happened under the new relationship.

It also makes deletion, retention, export, and investigation paths explicit. Those operational paths are where incomplete tenancy models are often exposed.

9. Test the negative space

Happy-path tests prove that an advisor can open an assigned client. They do not prove tenant isolation.

The valuable test matrix includes:

Scenario Expected result
Advisor with active membership and assignment reads assigned client Allowed
Advisor reads same-tenant but unassigned client Not found
Advisor reads client in another tenant Not found
Advisor has no active membership Empty workspace or denied
Advisor belongs to suspended organization Empty workspace or denied
Support reads bounded recovery context Allowed
Support attempts advisor/admin mutation Forbidden
Client changes browser-side organization ID No change in authority
Ended assignment is replayed Denied

The test should assert more than status codes. It should inspect the returned organization and assignment context, the audit scope, and the absence of cross-tenant rows.

The denial is a product behavior. It deserves an acceptance test.

10. What application checks do not solve

This design uses application-level tenant enforcement with database constraints. It does not claim that every category of isolation failure is impossible.

PostgreSQL row-level security could provide additional defense in depth, but it would introduce its own policy, connection, migration, testing, and operational complexity. Adding it before application context is consistent can create false confidence or make support operations difficult to reason about.

The sequence I prefer is:

  1. make the authorization model explicit;
  2. centralize context resolution;
  3. scope every query and write;
  4. add data constraints;
  5. prove denials through integration tests;
  6. then evaluate row-level security against concrete residual risks.

Defense in depth is most useful when each layer has a clear responsibility.

11. Practical design rules

  1. Keep platform role, tenant membership, and business relationship distinct.
  2. Treat request IDs and browser state as target selectors, never authority.
  3. Resolve authorization context on the server for every request.
  4. Deny by default when any required context is absent or inactive.
  5. Use not-found responses when existence is itself sensitive.
  6. Create useful support-safe paths instead of giving support admin power.
  7. Keep entitlement separate from identity and relationship history.
  8. Store historical organization, relationship, subject, and actor context.
  9. Enforce unambiguous active-state rules in the database.
  10. Test cross-tenant, unassigned, inactive, and stale-context cases explicitly.

Conclusion

Roles are labels. Authorization is a decision.

The decision becomes trustworthy when it is derived from authenticated identity, active tenant context, explicit relationships, resource ownership, requested action, entitlement, and current proof—not from a role name or a value supplied by the client.

This model requires more work than a role check. It also produces a system that can explain why access was granted, why it was denied, which relationship was in force, and what evidence should remain.

That is the difference between having roles and having an authorization architecture.

References

Footnotes

  1. OWASP Cheat Sheet Series, “Authorization Cheat Sheet”. ↩

  2. PostgreSQL Documentation, “Partial Indexes”. ↩

Citation sources

  1. OWASP Authorization Cheat Sheet · Return to citation
  2. PostgreSQL partial-index documentation · Return to citation

Related project