Admin or Member Isn't RBAC: A SaaS Maturity Model
Share this post

Admin or Member Is Not a Role-Based Access Control Strategy

A security questionnaire is usually the least dramatic document in a sales cycle. It arrives as a spreadsheet or a form in a third-party portal, gets routed to whoever answered the last one, and is filled out in an afternoon with answers copied from the previous round. Most of the two hundred rows are routine: encryption at rest, backup frequency, incident response contacts, sub-processor lists. Then, somewhere around row 40, there is a question that does not have a copy-paste answer.

"Describe how your application enforces separation of duties and least-privilege access for users within a single customer account. Can administrative privileges be scoped to specific teams, business units, or resource sets rather than granted account-wide?"

The honest answer, for a large number of B2B SaaS products at the point they first face a question like this, is: no. There is an "admin" role that can do everything, and a "member" role that can do the ordinary work. Anyone the admin invites and marks as admin becomes, functionally, a second admin — full access to every customer, every record, every configuration screen, every export. There is no way to make someone an administrator over just the East Region sales team, or just the billing configuration, or just user management without data export rights. The product does not lack a feature. It lacks a model.

This is where the story usually stops being about engineering and starts being about revenue. The prospect's security team does not reject the vendor outright — that would at least be fast. Instead the deal stalls. A call gets scheduled between the vendor's solutions engineer and the customer's IT security lead. The vendor promises "custom roles are on our roadmap." The customer's procurement team, which has its own quarterly targets and its own audit obligations, quietly moves the deal to next quarter, then deprioritizes it while the vendor's sales team keeps reporting it as "in security review" for months. Nobody writes an incident report, because nothing broke. A number on a pipeline dashboard just got smaller, and the reason is buried four steps upstream in a decision nobody remembers making.

That decision was made in the product's first year, usually by one or two engineers building an MVP, and it was almost certainly correct at the time. This article is about what happens between that first correct decision and the moment it becomes a liability, why the failure is structural rather than a matter of missing a feature request, and what an engineering organization can actually do about it — incrementally, without waiting for a deal to force the issue and without over-building for scale the company does not yet have.

Why Two Roles Felt Like the Right Amount for a Long Time

Every SaaS product's first paying customers look roughly the same: a founder or a single decision-maker signs up, invites two or three colleagues, and everyone on the account trusts everyone else because they all report to the same person or sit in the same room. In that context, "admin" and "member" is not a compromise. It is an accurate model of the organization using the product. The admin is whoever set up the account and is responsible for the bill. The member is everyone else. There is no meaningful distinction to draw between a marketing coordinator and a sales rep on a ten-person team, because neither of them is going to be restricted from anything the other can do, and building the machinery to restrict them would be pure cost with no corresponding benefit.

The binary model also has a specific engineering virtue: it is nearly impossible to get wrong. A single boolean — is_admin — is easy to reason about, easy to test, and cheap to implement. There is no role hierarchy to design, no permission inheritance to debug, no question about what happens when someone holds two roles at once. For a team moving fast toward product-market fit, this is exactly the right trade-off. Spending engineering time on a permission system nobody has asked for, while the core product still has gaps that block trial conversions, would be a bad allocation of scarce effort.

The problem is not that this decision was wrong. The problem is where it lives afterward. A schema decision, a choice of database, a decision about how the API is versioned — these get revisited on a schedule, because everyone can see them and everyone understands they are structural. A permission model, by contrast, usually gets built as a side effect of a different feature: "let people invite teammates" or "add a settings page." It is rarely reviewed with the rigor of an architecture decision because it was never framed as one. It sits quietly inside the account-management code, unowned, until a customer's organizational structure stops matching what it assumes.

That mismatch tends to appear in a predictable order. First, a customer wants to remove one specific person's ability to delete records without also removing their ability to view them — the two-role model has no way to express "can view, cannot delete." Then a customer with two departments using the product wants a manager in each department to administer their own team without seeing the other department's data — the model has no concept of scope narrower than the whole account. Then a customer's IT organization, evaluating the product as a vendor rather than as an end user, asks the separation-of-duties question above, and the answer reveals that the gap is not a missing checkbox but a missing dimension: role, resource, and scope were never separated from each other in the first place.

None of these moments individually look like a crisis. Each one looks like a reasonable feature request that got a workaround: a hardcoded exception for one customer, a hidden flag, a manual process where support staff adjust a database record by hand. The workarounds are what actually cause the eventual expense, because each one is a small, undocumented departure from whatever the "real" permission model is supposed to be. By the time an organization notices it has a permission-model problem, it usually has a dozen of these exceptions distributed across the codebase, each defensible on its own and collectively impossible to reason about.

What Role-Based Access Control Actually Promises, and What Two Roles Deliver

It is worth being precise about what "role-based access control" means, because the term gets used loosely enough that a two-role system and a genuinely role-based architecture both end up called "RBAC" in casual conversation, and that looseness is part of why the gap goes unnoticed.

The formal definition, codified in the American National Standards Institute / International Committee for Information Technology Standards specification INCITS 359 and built on research NIST published starting in the early 1990s, defines RBAC around a specific set of relationships: users are assigned to roles, roles are assigned permissions, and permissions authorize operations on protected objects. INCITS 359's reference model explicitly includes role hierarchies (a "Regional Manager" role inheriting everything a "Manager" role can do, plus more) and separation-of-duty constraints — both static constraints (a user can never hold two conflicting roles, such as "invoice creator" and "invoice approver," at the same time) and dynamic constraints (a user can hold both roles but cannot activate both in the same transaction, so the same person cannot create and approve the same invoice). NIST's own glossary defines RBAC as controlling access to resources through "permitted actions on resources... identified with roles rather than with individual subject identities," where a role functions as a reusable grouping of permissions aligned with a job function.

Two things fall out of that definition immediately. First, a system with exactly two roles, where one role holds every permission and the other holds a fixed subset, technically satisfies the letter of the definition — but it forfeits almost everything RBAC was designed to provide. Role hierarchies, separation of duties, and the ability to model an organization's actual division of labor all require more than two points on the map. Second, RBAC was never intended to be the final word on access control. It was designed to make administration easier by grouping permissions into roles instead of assigning them to individuals one at a time — a real improvement over pure discretionary access control, but one with known limits once an organization's structure gets complicated enough that "role" alone stops capturing who should be able to touch what.

Two more models fill in where RBAC's limits show up in practice.

Attribute-based access control (ABAC) makes access decisions from a combination of attributes about the user, the resource, and the context, rather than from role membership alone. A rule might read: allow this action if the user's department attribute matches the resource's department attribute, and the user's seniority level is at or above the resource's sensitivity level, and the request originates during business hours from a managed device. ABAC does not replace roles — role can be one of the attributes evaluated — but it lets a policy reason about relationships and conditions that a role name alone cannot express.

Relationship-based access control (ReBAC) goes further and makes the relationship itself the unit of authorization, typically expressed as a graph: a user is a member of a team, a team is assigned to a region, a region contains a set of documents, and a permission check walks that graph to answer "does this user have any path to this resource that grants the requested action?" This is the model Google formalized and published as Zanzibar, the authorization system behind Calendar, Drive, Photos, and YouTube, described in Google's own research publication as providing "a uniform data model and configuration language for expressing a wide range of access control policies" across services used by billions of people, while maintaining external consistency — meaning authorization decisions respect the order in which permission changes and content changes actually happened, so a user can never briefly see a document because a permission change and a content update raced each other. Zanzibar's production numbers, as published by Google, are worth noting because they establish what "authorization at scale" actually requires as a floor, not a ceiling: the system evaluates access checks against trillions of stored relationship tuples, serves millions of requests per second, holds 95th-percentile latency under 10 milliseconds, and has sustained greater than 99.999% availability over multiple years in production.

The industry's own security guidance has converged on a similar point about limits. OWASP's Authorization Cheat Sheet — the practitioner-facing standard maintained by the same organization that tracks broken access control as the industry's most common vulnerability class — states plainly that RBAC "is prone to errors, especially with role hierarchies," and that "in large projects or when numerous roles are present, it is easy to miss or improperly perform role checks," recommending ABAC and ReBAC as more expressive and more maintainable as systems grow, precisely because they decouple the permission model from a fixed, ever-multiplying list of named roles.

None of this means every SaaS company needs a Zanzibar-scale relationship graph on day one — a point this article returns to directly later, because reaching for that architecture too early is its own mistake. It does mean that "we have RBAC" is not a single fixed state. It is a spectrum, and knowing where a product actually sits on it — rather than assuming two roles counts as an entry-level implementation of the real thing — is the first step in deciding what to do next.

Model Unit of authorization Typical rule shape Where it excels Where it struggles Representative tooling
RBAC (Role-Based Access Control) Role membership "Users with role X can perform action Y" Simple org structures, small and stable permission sets, easy auditing of "who has role X" Role explosion as exceptions accumulate; cannot express per-resource scope without new roles for every scope In-app role tables; identity providers' native group-to-role mapping
ABAC (Attribute-Based Access Control) Attributes of user, resource, and context "Allow if user.department == resource.department AND user.clearance >= resource.sensitivity" Conditional, contextual rules (time, location, device, data classification) without a combinatorial role list Policies can become hard to audit in plain language; requires reliable attribute data Open Policy Agent / Rego, AWS IAM condition keys, Cedar
ReBAC (Relationship-Based Access Control) Graph relationships between entities "Allow if a path exists: user → member of → team → assigned to → resource" Nested organizational structures, resource hierarchies, sharing and delegation patterns (folders, projects, teams of teams) Requires modeling relationships explicitly; graph traversal at scale needs purpose-built infrastructure Google Zanzibar (internal), OpenFGA, Ory Keto, SpiceDB

Where Authorization Logic Actually Lives (and Where It Leaks)

The two-role model's deeper problem is not the shortage of roles. It is where the decision about who can do what actually gets made in the code, and how that location multiplies as the product grows.

In a typical early implementation, an authorization check looks something like this, scattered across whichever file happens to need it:

javascript
// billing-controller.js
if (currentUser.role === 'admin') {
  allowRefund();
}

// reports-controller.js
if (currentUser.role === 'admin' || currentUser.role === 'owner') {
  allowExport();
}

// user-management.js
if (currentUser.isAdmin) {
  allowInviteUser();
}

// admin-panel/settings.js
const ADMIN_ROLES = ['admin', 'superadmin'];
if (ADMIN_ROLES.includes(currentUser.role)) {
  allowChangeSSOConfig();
}

Each of these checks is individually reasonable, written by a different engineer, on a different date, to solve a specific feature's access requirement. None of them is wrong in isolation. Collectively, they are four different sources of truth about what "admin" means, expressed with four different syntaxes (===, ||, .isAdmin, an array .includes()), any of which can drift from the others the next time someone adds a role, renames one, or needs to add a fifth. There is no single place to answer the question "what can an admin actually do in this product?" except by searching the codebase for every string that might represent a role check — and that search has no reliable way to confirm it found all of them. A missed check is not a compile error. It is a runtime hole that behaves exactly like a working feature until someone without the intended permission stumbles into it.

This pattern is not a hypothetical failure mode. OWASP's 2025 revision of its Top 10 web application security risks lists Broken Access Control as the single most common category across its entire dataset: every application in the contributed testing data had at least one weakness in this category, spanning 40 distinct underlying weakness types (CWEs), with over 1.8 million occurrences and more than 32,000 associated CVEs in the aggregated data OWASP analyzed. Scattered, ad hoc authorization checks — the pattern above — are a primary structural cause, because each check is a separate opportunity to get the logic slightly wrong, and there is no central point where a reviewer, a test suite, or an auditor can verify all of them at once.

The architectural fix that both OWASP's guidance and the major authorization systems converge on is to separate where a permission decision is made from where it is enforced. The vocabulary for this, originally from XACML (the OASIS eXtensible Access Control Markup Language standard) and now standard across the authorization tooling ecosystem, splits the problem into three roles:

  • Policy Enforcement Point (PEP) — the code at the edge of a request (an API gateway, a controller, a middleware layer) that intercepts an action and asks whether it is allowed, then enforces the answer.
  • Policy Decision Point (PDP) — a single component whose only job is to answer "is this user allowed to perform this action on this resource?" by evaluating a policy against the request.
  • Policy Information Point (PIP) — the source of the facts the PDP needs to make that decision: role assignments, group memberships, resource ownership, organizational relationships.

Under this pattern, the four checks scattered across the earlier code sample collapse into four call sites that all ask the same question of the same authority:

javascript
// billing-controller.js
if (await authz.can(currentUser, 'refund', invoice)) { allowRefund(); }

// reports-controller.js
if (await authz.can(currentUser, 'export', report)) { allowExport(); }

// user-management.js
if (await authz.can(currentUser, 'invite_user', account)) { allowInviteUser(); }

// admin-panel/settings.js
if (await authz.can(currentUser, 'change_sso_config', account)) { allowChangeSSOConfig(); }

The permission logic itself now lives in exactly one place — a policy definition, not four independently maintained conditionals — which means it can be read, tested, and audited as a single artifact. It also means adding a new role, narrowing an existing one, or introducing resource-level scoping is a change to one policy file rather than a hunt through the application for every place someone might have written a role check by hand. This is not a cosmetic refactor. It is the difference between a permission model that can be reasoned about and one that can only be discovered by exhaustively testing every code path — which, as the testing section below covers, almost never actually happens.

The Risk Map: Where a Weak Permission Model Actually Costs You

The consequences of the gap above do not land in one place. They surface differently depending on which part of the organization encounters them first, and the risk map below is built around that distinction, because the fix that satisfies a sales engineer is not the same fix that satisfies a security auditor.

Capability gap Where it surfaces first What the buyer or auditor is actually asking Typical business consequence Deal stage most affected
No custom roles beyond admin/member Sales / solutions engineering "Can we give our support lead admin access without giving them billing access?" Feature-parity objection; buyer perceives the product as built for smaller teams Mid-funnel demo, competitive evaluation
No resource-level or team-level scoping Sales / solutions engineering, then security "Can an admin in Region A be prevented from seeing Region B's data?" Multi-department or multi-subsidiary buyers cannot adopt a single shared account Proof-of-concept, expansion within existing account
No separation-of-duties enforcement Security review, internal audit "Can the same person create and approve a financial transaction?" Direct SOC 2 / internal-controls failure for finance, healthcare, and regulated buyers Formal security review, procurement
No delegated administration IT / identity team "Can our own admins manage our own users without contacting your support team?" Operational friction post-sale; support burden shifts to vendor indefinitely Onboarding, renewal
No SSO-driven group-to-role mapping IT / identity team "When we remove someone from our identity provider, does their access here disappear automatically?" Offboarding risk; a departed employee retaining access is a reportable finding in many audits Security review, contract renewal
No queryable audit trail of permission grants and changes Security review, compliance "Show us, for any user, every permission they hold and when and by whom it was granted." Audit finding; in regulated industries, a blocking finding rather than a recommendation Formal security review, compliance certification

Two things about this table are worth pulling out explicitly. First, the earliest gaps in the list are sales problems — a competitive feature comparison the buyer notices during a demo. The later gaps are compliance problems, and compliance problems do not get negotiated away with a roadmap slide, because the person evaluating them is often not empowered to accept "coming soon" as an answer; their job is specifically to say no to unmet controls. Second, the progression down the table roughly tracks company size: a twenty-person customer rarely asks about separation of duties, and a five-thousand-person customer rarely tolerates its absence. A permission model that stalls at the top rows of this table will simply stop being sellable past a certain account size, regardless of how good the rest of the product is.

The scale of the underlying problem, independent of any one company's roadmap, shows up in how the security industry itself measures it. OWASP's 2025 Top 10 dataset — drawn from contributed application testing data across its analysis period — puts broken access control at the top of the list by a wide margin, and pairs that prevalence with a specific, uncomfortable gap between how common the weakness is and how well it is actually tested for.

Chart 1 — Broken Access Control: Prevalence vs. Average Test Coverage

Metric Value Source
Applications in the dataset with at least one access-control weakness 100% OWASP Top 10:2025, A01
Distinct CWEs (weakness types) mapped to this category 40 OWASP Top 10:2025, A01
Total occurrences across contributed testing data 1,839,701 OWASP Top 10:2025, A01
CVEs mapped to this category 32,654 OWASP Top 10:2025, A01
Average incidence rate across tested applications 3.74% OWASP Top 10:2025, A01
Average test coverage for this category 42.93% OWASP Top 10:2025, A01
 
Applications affected (100%)   ████████████████████████████████████████
Average test coverage (42.93%) █████████████████░░░░░░░░░░░░░░░░░░░░░░░
                                0%        25%        50%        75%     100%

What this shows: every application in OWASP's dataset had some form of broken access control, but on average, testing only covered access-control paths about 43% of the time. That gap — a defect category that is both universal and chronically under-tested — is the same gap this article's testing section addresses directly: permission logic is not undertested because teams are careless, it is undertested because the combinatorial space is large and the tooling most teams have does not make that space visible. Source: OWASP Top 10:2025 — A01:2025 Broken Access Control.

A Maturity Model for Permission Systems

There is no single "correct" permission architecture independent of company stage — building a Zanzibar-scale relationship graph for a ten-person startup would be as much of a mistake as staying on a hardcoded admin/member binary at two thousand employees. What matters is recognizing which stage a product is actually in, what specifically breaks at the boundary to the next stage, and whether the next stage is being designed deliberately or discovered under deal pressure.

Stage Defining capability How access is granted Audit capability Typical company stage What forces the move to the next stage
1. Binary Admin or member; admin means "everything" Manual invite, role set at invite time None beyond login history Pre-seed to early seed; single-team accounts A customer with more than one internal team, or any regulated-industry buyer
2. Hardcoded roles A small, fixed set of named roles (e.g., admin, editor, viewer) baked into the codebase Manual invite with role selection from a fixed list Role assignment visible per user, but not historized Series A–B; product has distinct job functions (e.g., "billing" vs. "content") A customer needs a role the fixed set does not express, or two customers need conflicting definitions of the same role name
3. Custom roles Account admins can define new roles by composing permissions from a fixed permission catalog Self-service role builder within the account Per-role permission list is queryable; per-user role history often still missing Series B–C; multiple mid-market accounts with internal structure A customer needs permissions scoped to a subset of resources (a team, a region, a project), not just a subset of actions
4. Resource-scoped permissions Permissions can be limited to a subset of resources — a team, a project, a business unit — not just a subset of actions Role plus scope assignment (e.g., "editor, Region: EU-West") Scope and role both queryable and historized Growth stage to early enterprise; first regulated or multi-division customers A customer's identity provider needs to drive role and scope assignment automatically, and internal audit needs a full, exportable permission history
5. Delegated, policy-driven administration Centralized policy definitions (not per-user database rows) drive authorization; account admins can delegate scoped administrative rights to sub-admins; group membership at the identity provider maps automatically to in-app scope SSO/SCIM-driven group-to-role-and-scope mapping, reviewable and centrally versioned Full, queryable, exportable audit trail of every grant, change, and access decision, tied to policy version Enterprise-serving at scale; regulated industries; accounts with hundreds of internal users Rarely forced further by a single deal — this is the ceiling most B2B SaaS products need, reached deliberately rather than reactively

Two patterns in this table matter more than the stage labels themselves. The first is that stages 1 through 3 are about actions — what a role is permitted to do — while stages 4 and 5 introduce scope: what subset of the account's data or resources those actions apply to. This is the single most common gap between what a growing SaaS product has built and what an enterprise buyer needs, because scope is a genuinely different problem from action, not a harder version of the same one. A role system with fifty finely-differentiated action permissions but no scoping still cannot answer "can this person manage users in the EU office but not the US office," because the question is not about what actions someone can take — it's about which slice of the resource graph those actions apart apply to.

The second pattern is that the jump from Stage 4 to Stage 5 is rarely about adding more permission granularity. It is about moving the source of truth for role assignment out of the application's own user-invite flow and into the customer's identity infrastructure — which is a organizational and integration change as much as a permissions one, covered in more detail later in this article.

Stage 1Binaryadmin / memberStage 2Hardcoded rolesfixed role listStage 3Custom rolesself-service role builderStage 4Resource-scopedroles + scope(team/region/project)Stage 5Delegated, policy-drivenSSO/SCIM group mapping,centralized policy, full audittrail

Three Situations Where the Model Breaks in Practice

The maturity model above describes the shape of the problem in the abstract. The three scenarios below are hypothetical — built to illustrate specific, recurring failure patterns, not descriptions of any real company or product — but each one reflects a pattern that shows up repeatedly across B2B SaaS categories once a company starts selling to organizations with real internal structure.

A B2B Marketplace's Single-Admin Bottleneck (Hypothetical)

Initial situation. A B2B marketplace platform connects buyers and suppliers, and its account model was built around a simple assumption: one company signs up, one person from that company is the admin, and everyone else on their team is a member with read/write access to listings and orders. This worked cleanly through the platform's first two years of customers, most of which were small procurement teams of five to fifteen people.

The hidden assumption. The model assumed "one company equals one administrative unit." It had no representation of a company with multiple, semi-independent business units — a holding company with three regional subsidiaries, each running its own supplier relationships, each wanting its own local administrator, none of them wanting the others' administrators to see their contract terms.

The consequence. A large enterprise prospect — a multinational buyer with exactly this regional structure — evaluated the platform and hit the wall immediately: giving each region's operations lead admin access meant giving them visibility into every other region's supplier pricing, which the prospect's legal and procurement teams flagged as a non-starter for a platform intended to keep regional negotiations confidential from each other. The workaround anyone might reach for first — creating three entirely separate accounts under three separate contracts — solved the visibility problem but broke the platform's own value proposition of a unified supplier network view, and created three sets of billing, onboarding, and support relationships where the customer wanted one.

The decision point. The engineering team had to choose between a fast, narrow patch (a one-off flag limiting a specific admin's dashboard to a specific region, hardcoded for this one account) and a structural fix (introducing an organizational unit above "account" and below "company," with roles and admin rights scoped to that unit). The narrow patch would have closed this specific deal faster. It would also have created exactly the kind of undocumented, per-customer exception described earlier in this article — a special case future engineers would need to rediscover before touching the permission code again.

The better approach. Modeling the organizational unit as a first-class resource — not a per-customer hack — meant the fix generalized to every future customer with the same structure, and it aligned with Stage 4 of the maturity model above: scope, not just action, became something the permission system could express.

A Health-Tech Vendor's Location-Scoped Records Problem (Hypothetical)

Initial situation. A clinical workflow SaaS product used by outpatient clinics started with two roles: "clinician," who could view and edit patient records, and "front desk," who could schedule appointments but not view clinical notes. This matched the needs of its first customers, independent single-location practices.

The hidden assumption. The model assumed every clinician in a customer account should be able to see every patient record in that account, because at a single-location practice, that was functionally true — the clinicians on staff were, in practice, all involved in some overlapping subset of patient care.

The consequence. A multi-location hospital network adopted the product and immediately ran into HIPAA's "minimum necessary" standard for accessing protected health information: a clinician at Location A had no clinical reason to access records for a patient who had only ever been seen at Location C, and the customer's compliance team required the product to enforce that boundary technically, not just as a written policy clinicians were expected to follow voluntarily. The product's two-role, account-wide model had no concept of "location" as a scope at all — a clinician role was a clinician role everywhere in the account.

The decision point. Bolting on a location filter at the UI layer — hiding records from other locations in the interface — would have addressed the visible symptom while leaving the underlying API fully queryable across locations, which would not satisfy a compliance team asking specifically how the enforcement worked, not how the interface looked. The real fix required the permission check to happen at the same layer that served the data, not the layer that displayed it.

The better approach. This is a natural fit for relationship-based modeling rather than a bigger role list: a clinician is assigned to one or more locations, a patient record belongs to a location, and the permission check walks that relationship rather than checking a role name. Expressed as the kind of relationship tuples a system like OpenFGA or a Zanzibar-derived model stores:

user:dr_patel is assigned_to location:downtown_clinic
user:dr_patel is assigned_to location:westside_clinic
patient_record:48213 belongs_to location:downtown_clinic
patient_record:60110 belongs_to location:eastside_clinic

# Permission check: can dr_patel view patient_record:60110?
# → No path exists from dr_patel to eastside_clinic → denied.

Adding a third location later, or reassigning a clinician between locations, becomes a change to the relationship graph rather than a change to application code — the same structural benefit the marketplace example gained by treating scope as a resource rather than a special case.

A Fintech Platform's Missing Segregation of Duties (Hypothetical)

Initial situation. A B2B expense-management platform let any user with the "finance" role create a payment request and approve it. Most of its early customers were small companies where the same one or two finance staff handled the entire process end to end, and requiring a second approver would have meant hiring someone specifically to click "approve."

The hidden assumption. The model assumed the person best positioned to catch an error or a fraudulent request was the same person who created it — which is precisely the assumption that segregation-of-duties controls exist to eliminate, because it fails exactly when it matters most: when the request is wrong, self-dealing, or fraudulent, the creator has no incentive to catch it.

The consequence. A customer's external auditor, performing a routine SOC 2 Type II examination, flagged that the platform allowed a single user to both initiate and approve the same payment — a direct finding against a standard internal control (often labeled a maker-checker control), independent of whether any actual misuse had occurred. The customer had to report the finding, and their own security team escalated a request to the vendor: either the platform needed to enforce segregation of duties technically, or the customer would need to build a manual, off-platform approval step that undermined the reason they had bought the platform in the first place.

The decision point. A quick fix — a warning message suggesting a different approver, easily dismissed — would have satisfied nobody: not the auditor, who needed enforcement, not the security team, who needed the platform to make the violation structurally impossible rather than merely discouraged.

The better approach. This is the static separation-of-duty constraint INCITS 359 describes directly: the policy needs to compare the identity of the request's creator against the identity of the proposed approver, at the resource level, not the role level — an ABAC-shaped rule ("deny if approver.user_id == request.creator_id"), layered on top of the existing role check rather than replacing it. Getting this right required the authorization logic to have access to resource-level facts (who created this specific request), which is exactly the kind of check that scattered, role-only conditionals cannot express, because role alone answers "is this a finance person," not "is this the same finance person who created this specific request."

The Testing Problem: Why Permission Logic Is Chronically Undertested

The OWASP data cited earlier — every application in the dataset carrying an access-control weakness, average test coverage sitting under 43% for the category — is not a story about careless teams. It is a story about a defect category whose test space grows multiplicatively while most testing practice grows linearly.

A functional test suite naturally tests actions: does creating an invoice work, does exporting a report produce the right file, does deleting a record actually delete it. Permission testing asks a categorically different and much larger question for every one of those actions: for each role, and for each resource this action touches, should this specific combination be allowed or denied — and is the denial actually enforced, not just hidden in the UI?

A small illustrative calculation makes the scale concrete. A moderately mature product with 6 distinct roles, 12 resource types, and 4 possible actions per resource (view, create, edit, delete) already has 6 × 12 × 4 = 288 role-action-resource combinations that a complete permission test suite would need to account for — and that number assumes no resource-level scoping at all. The moment the product adds scope (Stage 4 of the maturity model above), the same customer account with 20 internal teams multiplies that further: a permission that is correct for a user assigned to Team A must also be verified as denied for the identical role assigned to Team B, which is 288 × 20 = 5,760 effective states for a single customer account, most of which a conventional "does the happy path work" test suite never touches, because the happy path only exercises the combinations someone thought to click through manually.

This is the deeper reason permission logic is chronically undertested: it is not that testers are careless about a known risk, it is that the risk is combinatorial, invisible in a UI walkthrough, and does not fail loudly. A missing negative-permission check does not throw an error. It quietly allows something that should have been denied, and the only way to notice is to have deliberately tested for the denial in the first place.

Chart 2 — Illustrative Relative Engineering Effort to Retrofit Access Control, by Company Stage

The figures below are a hypothetical, illustrative scenario built for this article to communicate directionality. They are not measured benchmark data from any real company, product, or research study — no reliable industry-wide dataset exists for "cost to retrofit a permission model," because the cost depends heavily on codebase size, data volume, and how many workaround exceptions have accumulated. Treat the relative ordering, not the specific multipliers, as the point.

Company stage at time of retrofit Illustrative relative engineering effort (Stage 1 = baseline) Why the multiplier grows
Early stage (binary roles, small codebase, no live enterprise data) 1x Small surface area; no production data migration required
Growth stage (first custom roles added) 3x More call sites to update; some backward compatibility needed
Mid-market entry (resource-level scoping requested) 8x Existing data has no scope field; requires backfill and migration against live customer data
Enterprise entry (delegated admin, SSO group mapping, full audit trail required) 20x Integration work with customer identity providers; audit trail must be retroactively reconstructed or accepted as incomplete
Post-security-review-failure retrofit under deal pressure 35x+ Same technical work as above, compressed into a deadline set by a sales cycle, against a live system with real customer data and no room for a phased rollout
 
Early stage        █
Growth stage        ███
Mid-market entry    ████████
Enterprise entry     ████████████████████
Post-failure retrofit ███████████████████████████████████ +
                     0    5    10   15   20   25   30   35
                          (illustrative relative effort, Stage 1 = 1x)

What this shows, as an illustrative scenario: the cost of building proper scoping and delegation does not grow linearly with company size — it grows fastest exactly at the point where the fix has to happen against live, populated customer data under a deadline someone else set, rather than as planned engineering work with time to design a clean migration.

A practical framework for closing the coverage gap, rather than trying to write every one of the thousands of combinations by hand, is to test the structure of the permission system rather than enumerate every instance of it:

A staged approach to testing a permission system

  1. Enumerate the matrix explicitly. Write down every role, every resource type, and every action as a table, not as a mental model. If no one can produce this table in under five minutes, that is itself a finding — it means the permission model is not centralized enough to be documented.
  2. Test the negative space, not just the happy path. For every role, assert what it cannot do against every resource type it touches, not only what it can. A test suite with only positive assertions will pass even when a denial check silently regressed.
  3. Test at the API layer, independent of the UI. A hidden button is not a security control. Every permission test that only clicks through the interface is testing the UI's opinion of the user's permissions, not the enforcement layer's.
  4. Test state transitions, not just static assignments. Role changes mid-session, a user removed from a team while they have an open browser tab, a resource transferred to a new owner — these are exactly the moments cache staleness and stale session tokens turn a correct static model into an incorrect running one.
  5. Track authorization test coverage as its own named metric. Folding permission tests into overall code coverage hides the gap OWASP's data highlights — a codebase can carry high overall coverage while its access-control paths sit near the 43% average cited above.
  6. Add a permission regression test the moment a new resource type ships. A new resource type without a corresponding row in the enumerated matrix is a silent gap by construction, not an edge case.
  7. Run a periodic access review against production data, not just code. Code review confirms the policy is written correctly; a production access review confirms it was actually applied — catching the manual database overrides and one-off exceptions that never went through the policy at all.

Engineering Approaches: From Scattered Checks to a Policy Engine

Once the decision is made to centralize authorization rather than leave it scattered across the codebase, the practical question is which architecture fits the company's actual scale and complexity — not which one is most sophisticated in the abstract.

Approach How it works Best fit Operational cost Example
In-app scattered checks Role checks written directly in application code wherever needed Very early stage; two roles; no scoping needs Lowest upfront, highest long-term (the pattern this article argues against past a certain size) if (user.role === 'admin')
Shared authorization library / middleware A single internal library or middleware function all code paths call, still running in-process Growth stage; custom roles; single codebase or monorepo Low; keeps logic co-located with the app but centralizes the decision Internal can(user, action, resource) helper used everywhere
Centralized policy engine Authorization logic externalized into declarative policy files, evaluated by a dedicated engine the application queries Multiple services or microservices need consistent decisions; ABAC-style contextual rules Moderate; requires learning a policy language and versioning policies like code Open Policy Agent (OPA) with Rego policies
Relationship-based authorization service Permissions modeled as a relationship graph (Zanzibar-style), queried as a service, often with its own low-latency store Complex nested organizational structures, resource hierarchies, delegation and sharing patterns at scale Highest; a dedicated service, schema (relationship model), and its own reliability requirements Google's internal Zanzibar; OpenFGA (CNCF Incubating project, originated at Auth0/Okta); SpiceDB; Ory Keto

Open Policy Agent, maintained as an open-source, general-purpose policy engine, illustrates the middle option concretely. Instead of scattering conditionals through application code, a policy is written declaratively in OPA's policy language, Rego, and the application queries the policy engine — often over a local HTTP endpoint per OPA's documented architecture — with a request describing the user, the action, and the resource:

rego
package authz

import future.keywords.in

default allow := false

# A finance approver can approve an invoice, unless they created it themselves.
allow if {
    input.action == "approve_invoice"
    "finance_approver" in data.user_roles[input.user]
    input.invoice.created_by != input.user
}

# Admins can manage users only within their assigned business unit.
allow if {
    input.action == "manage_user"
    "admin" in data.user_roles[input.user]
    data.user_business_unit[input.user] == data.resource_business_unit[input.resource]
}

The separation-of-duties rule from the fintech example above and the business-unit scoping rule from the marketplace example above are both expressible as policy, in one file, independent of which controller or service happens to trigger the check — which is precisely the property that scattered in-code conditionals cannot provide, because a policy file can be reviewed, tested, and versioned as a single unit, the way application code already is.

For organizations whose permission problem is fundamentally about relationships and hierarchy rather than conditional rules — nested teams, shared folders, delegated ownership — a relationship-based system is a better structural fit than a rules engine, because the underlying question ("does a path exist from this user to this resource through some chain of relationships") is a graph query, not a boolean expression. OpenFGA, released as open source by Auth0 (now part of Okta) and since accepted as a Cloud Native Computing Foundation Incubating project, implements this model directly, explicitly built to bring Zanzibar's approach to teams that are not Google-scale but face the same underlying shape of problem: users, groups, resources, and the relationships between them, queried in real time as authorization decisions.

The return on this kind of investment is easiest to see by comparing what each approach actually saves. A shared authorization library mainly saves engineering time — fewer duplicated conditionals, fewer places for a role check to drift out of sync with the others. A centralized policy engine saves audit time as much as engineering time: when a customer's security team asks for evidence of how a specific control is enforced, a policy file that can be exported and read is a materially easier artifact to produce than a promise that the relevant conditionals were checked by hand across the codebase. A relationship-based service saves neither of those directly — its return shows up in product capability, specifically the ability to say yes to organizational structures (nested teams, shared ownership, delegated scopes) that a role-only system genuinely cannot represent regardless of how well-organized the code behind it is. None of these returns show up as a line item on a standard engineering ROI spreadsheet, which is part of why this work is chronically deprioritized against features with a more visible revenue attribution — until the specific deal it would have unblocked is the one that stalls.

None of these tools should be adopted because they are the most technically interesting option. The AWS Prescriptive Guidance documentation on implementing a policy decision point, and OPA's own security documentation, both frame the decision the same practical way: adopt the centralization pattern (PEP/PDP/PIP) as early as reasonable, because it is a cheap architectural discipline even with a small rule set, and defer the choice of which engine or service implements the PDP until the complexity of the rules or the relationship structure actually demands it.

Delegated Administration and the SSO/SCIM Problem

Stage 5 of the maturity model — delegated, policy-driven administration — depends on a piece of infrastructure most SaaS products only encounter once an enterprise deal specifically requires it: automated provisioning driven by the customer's own identity system, rather than manual invites managed inside the product.

The relevant standard is SCIM (System for Cross-domain Identity Management), formalized in IETF RFC 7644, which the specification itself describes as existing to "reduce the cost and complexity of user management operations by providing a common user schema, an extension model, and a service protocol." In practice, SCIM lets a customer's identity provider (Okta, Microsoft Entra ID, Google Workspace, and others) push user and group changes directly into a connected application: when an employee joins the "Finance" group in the identity provider, SCIM can automatically provision them into the application with whatever role that group maps to; when they leave the company, SCIM can automatically deprovision them, closing the exact offboarding gap flagged in the risk map earlier in this article.

This matters for the permission-model discussion specifically because SCIM only solves half the problem on its own. SCIM synchronizes who exists and what groups they belong to — it does not, by itself, decide what those groups should be allowed to do inside the application. That mapping (identity-provider group → in-app role and scope) has to be built on top of whatever permission architecture the earlier sections of this article describe. A product still running Stage 1 or Stage 2 of the maturity model can implement SCIM provisioning and still have nowhere meaningful to map incoming groups, because there are only two roles to map anything into. SCIM is the delivery mechanism for delegated administration; it is not a substitute for having a permission model sophisticated enough to receive what it delivers.

Delegated administration itself — letting a customer's own IT admin manage a subset of users or resources without contacting the vendor's support team — is the organizational half of Stage 5. It requires the resource-level scoping introduced at Stage 4 (an admin needs a scope to be an admin of) plus an explicit administrative-role concept that is itself scoped, rather than a single account-wide "admin" flag. Practically, this means the platform's own concept of "who can grant permissions to others" has to be represented in the same policy system as the permissions themselves — a scoped admin should only be able to grant roles and scopes within their own delegated boundary, which is again a natural fit for the relationship-based model (a regional admin is assigned to a region, and their administrative rights are constrained to members of that region) rather than something a flat role list can express cleanly.

Measuring Whether the Permission System Is Actually Working

Once a permission model moves past the binary stage, it becomes tempting to track its health with metrics that look reassuring on a dashboard but don't actually measure the thing that matters: whether access matches intent, and how quickly it stops matching intent once someone's role in the organization changes.

Metric What it appears to show What it actually measures A better alternative
Number of roles defined in the system Sophistication of the permission model Role sprawl — often a sign that new roles were created one-off to patch specific requests, rather than composed from a coherent permission catalog Number of roles that have been reviewed and consolidated in the last two quarters
Percentage of users with a "custom" (non-default) role Adoption of fine-grained access How often the default roles fail to fit real usage — a high number can indicate the base role set is wrong, not that customers are sophisticated Percentage of custom-role grants that still match their original justification after 90 days
Total permission checks executed per day System activity / usage Almost nothing about correctness — a check that always evaluates to "allow" produces the same volume as one enforcing a real boundary Percentage of permission checks enforced at the API layer versus only hidden in the UI
Time since the account's permission model last changed Stability Can mean the model is mature, or can mean nobody has revisited it since the organization using it changed shape Time since the last full access review or entitlement audit was actually performed against production data
Number of standing administrator accounts Administrative capacity How much of the account's total blast radius is permanently exposed if any one of those accounts is compromised Number of standing, non-expiring elevated-privilege grants with no scheduled review date

The most useful single number to track, for a growing SaaS product, is one rarely put on a dashboard: offboarding latency — the time between a person losing their reason to have access (leaving a company, changing teams, ending a contract) and that access actually being revoked across every system that granted it. This is the metric most enterprise security reviews probe indirectly through questions about SSO-driven deprovisioning, and it is one of the few permission-system metrics with a genuinely intuitive business meaning: a large number means real people retain real access to real customer data for longer than anyone intended, which is a liability regardless of whether it has ever been exploited.

A Diagnostic Checklist: Is Your Permission Model About to Block a Deal

The following signs, taken individually, are common and often fine at small scale. Taken together, they indicate a permission model that will surface as friction the next time a structurally complex buyer evaluates the product.

  • Your only two roles are "admin" and "member," and "admin" means "can do everything."
  • In the last two quarters, you've added a one-off boolean flag (can_export, is_regional_manager) to satisfy a specific customer request rather than extending the role model itself.
  • Removing someone's access requires deleting their user account entirely, because there is no way to revoke specific permissions while keeping the account active.
  • A departing employee's actual permissions can only be determined by reading application code, not by querying a permissions table.
  • Two people cannot hold different scopes of the same role — "regional admin" and "global admin" are not distinguishable in the system.
  • Your audit log records who logged in and when, but not who was permitted to perform a specific action, or why.
  • A prospect's security team has asked how you enforce least privilege, and the honest answer described an intention rather than a mechanism.
  • Group membership in a customer's identity provider (Okta, Entra ID, etc.) has no effect on that user's permissions inside your product.
  • Provisioning a new user's access depends on someone remembering to click "invite" with the right role, not on an automated, group-driven policy.
  • Support engineers can be granted temporary elevated access to a customer account, but there is no expiration or automatic revocation once the support ticket closes.

Three or more of these being true is a reasonable trigger to treat the permission architecture as a planned engineering investment rather than something to patch reactively the next time it blocks a deal.

When Not to Build This Yet

The temptation, once the risk map above is visible, is to treat Stage 5 — full delegated administration, SSO/SCIM-driven mapping, a centralized policy engine — as the target for every company regardless of size. That instinct is itself a mistake, and worth naming directly.

A ten-person startup selling to other ten-person startups has no organizational complexity for a permission system to model. Building a relationship-based authorization service for that customer base is not "designing for scale" — it is spending scarce engineering time on a problem the business does not have yet, at the direct expense of the product work that would actually win the next ten customers. The binary admin/member model is the correct engineering choice at that stage, not a shortcut to be ashamed of.

The judgment call that actually matters is not "when do we build the most sophisticated system," it is "when does the next stage of the maturity model start being requested by real buyers, and can we move one stage at a time rather than skipping straight to the end." A company whose customers are starting to have two or three internal teams needs Stage 3 (custom roles), not Stage 5. Jumping straight to a full relationship-based authorization service at that point adds operational complexity — a new service to run, a new query pattern to reason about, a new failure mode (the authorization service itself becoming a single point of failure) — without a corresponding need for it yet. The AWS and OPA guidance cited earlier makes this same point from the tooling side: centralize the pattern (a single decision point, one source of truth) early, because that is cheap, and defer the sophistication of the engine behind it until the actual rule complexity or relationship structure justifies the operational cost of running it.

The scale-versus-cost trade-off also runs the other way at the far end. A company already operating a Zanzibar-style relationship graph should be skeptical of requests to add role-based shortcuts back in for convenience — a "just make everyone in this group an admin" request is exactly the kind of scope creep that erodes a fine-grained model back toward the binary one this article opened with, just with more infrastructure underneath it.

Questions Worth Asking Before the Next Enterprise Deal Reaches Security Review

  • Can we produce, right now, a queryable list of exactly what a specific employee at a specific customer account is permitted to do, and why?
  • If our two most senior engineers were unavailable, could someone else safely add a new resource type without having to rediscover, by reading scattered code, how permissions are supposed to work?
  • How many places in the codebase currently contain a role check, and does anyone actually believe that count is complete?
  • What is our real answer, today, to a security questionnaire item asking about separation of duties for financial or sensitive actions?
  • Can a customer's own IT administrator delegate management of their users and teams without opening a support ticket with us?
  • When someone is removed from a customer's identity provider, does their access to our product disappear automatically, or does it depend on someone remembering to revoke it manually?

Frequently Asked Questions

What is the practical difference between RBAC, ABAC, and ReBAC? RBAC grants permissions based on named roles a user holds. ABAC evaluates rules against attributes of the user, resource, and context (department, sensitivity, time of day, and similar factors). ReBAC evaluates whether a relationship path exists between a user and a resource — membership in a team that is assigned to a project that owns a document, for example. Most mature systems combine elements of all three rather than picking exactly one.

Do we need Open Policy Agent or a Zanzibar-style system to sell to enterprise customers? Not necessarily, and not immediately. What enterprise buyers and security reviews actually require is resource-level scoping, delegated administration, SSO-driven provisioning, and an auditable permission history — Stages 4 and 5 of the maturity model in this article. Those capabilities can be built incrementally without adopting a specific external tool; the tools become worth their operational cost once the rule complexity or relationship structure genuinely exceeds what a well-organized internal policy layer can handle cleanly.

What do enterprise security questionnaires typically ask about permissions? Common items include whether administrative access can be scoped rather than granted account-wide, whether the system enforces separation of duties for sensitive actions, whether access is automatically revoked when a user is removed from the customer's identity provider, and whether a full audit trail exists showing who was granted what access, when, and by whom.

How many roles should a SaaS product start with? As few as the actual organizational structure of its customers requires — often two or three is genuinely correct at an early stage. The mistake is not starting simple; it is failing to notice when customers' organizational complexity has outgrown the simple model, and patching around that mismatch with one-off exceptions instead of deliberately moving to the next stage.

What is delegated administration, and why do enterprise buyers ask for it? Delegated administration lets a customer's own administrators manage a defined subset of users, teams, or resources without needing account-wide access or a request to the vendor's support team. Enterprise buyers ask for it because centralizing all administrative work with a vendor's support desk does not scale to organizations with hundreds of internal users and does not satisfy least-privilege requirements that limit how much access any single administrator holds.

Can a permission model be retrofitted without a full rewrite? Usually yes, if it is done as a staged migration rather than a single cutover: introducing a centralized decision point first (without changing the underlying role model), then adding scope as a new dimension, then connecting that scope to delegated administration and identity-provider-driven provisioning. Attempting to design and ship a complete Stage 5 system in one release, against live production data, is the pattern that makes retrofits expensive and risky — the incremental path is slower but avoids the highest-risk version of the change.

The Real Choice Isn't RBAC vs. ABAC

The technical comparison between RBAC, ABAC, and ReBAC is worth understanding, but it is not the decision that actually determines whether a permission model survives contact with a growing customer base. The decision that matters is simpler and gets made earlier: whether the organization treats its permission model as infrastructure — reviewed, owned, and evolved deliberately — or as a byproduct of whatever feature happened to need an access check first.

A binary admin/member model is not a mistake. Failing to notice when it has stopped matching the organizations buying the product is the mistake, and it is a quiet one, because nothing about it looks like a defect until a specific deal stalls or a specific audit finding lands, by which point the fix has to be built against live customer data instead of a clean design. The question worth taking back to an engineering team is not "which access control model is best" — it is "who owns this, and when did we last check whether it still matches how our customers are actually organized."

Weak access control does not announce itself. It shows up as a deal that goes quiet, a security review that stalls, or an audit finding that traces back to a decision nobody remembers making — which is exactly why it needs an owner before any of those things happen, not after.

Reviewing whether a permission model still matches how customers are actually structured is not a project that requires a dedicated team or a quarter of roadmap space — it is a periodic question, similar in spirit to a schema review, that belongs on someone's explicit list of responsibilities rather than left to surface on its own. QAtronic works with engineering teams on exactly this kind of review: mapping the current permission model against the maturity stages above, identifying which specific gaps are closest to blocking a real deal or failing a real audit, and building the test coverage needed to change the model safely against data that is already live.

Recent posts

September 4, 2026
Saga Compensation Testing: The Rollback No One Checks
September 4, 2026
Post-Acquisition Technical Integration: The First 100 Days
September 4, 2026
Why Coding Interviews Don't Predict Software Quality