Endpoints decide for themselves when nobody writes down who may do what, and their answers disagree. In my June and July 2026 audits, the Authorization pillar averages 42.1 out of 100 across the 14 third-party apps scored on it. These RBAC examples are 3 role matrices for a small SaaS, plus a test that the backend enforces every cell.

RBAC examples: three role matrices for a small SaaS

RBAC examples for a small SaaS are permission matrices: actions down the side, roles across the top, allow or deny in each cell, and a scope column. This page fills in 3 of them: a team workspace, a two-sided marketplace and an AI product. Each also says which role may grant which role.

Take any single row below as a role based access control example in miniature: a member may edit a record they created, and may not change anyone’s role, however the screen looks.

Those 14 apps were a selected set from my June and July 2026 audits, scored on the Authorization pillar where it applied, not a random sample and not a rate for AI-built apps in general. The matrix is one control in the authentication checklist, and writing a one-page matrix that defines roles and their permitted actions is deliverable 1.7 of the Production Hardening Sprint.

A permissions matrix has four parts. Each row is one action the API exposes, written as a verb on a noun: “invite a user”, “export all records”, “delete the workspace”. Each role gets a column with yes or no. The scope column says how far a yes reaches: the caller’s own record, their own tenant, or every record on the platform. The “who may grant” column names the role that can hand that permission to someone else, and a role that can grant itself upward is the first bug to look for. Rows come from what the API accepts, never from screens, because a hidden button still leaves the route open.

The cells below are my worked examples for these three shapes, not a standard. Copy the shape and change the cells to what your business wants. The invite, role-change and removal rows are the user management every one of these web applications needs, and they appear in all three.

Matrix A: a B2B team workspace

Four roles inside one customer’s workspace: owner, admin, member, viewer. “Admin” here means an admin inside one customer’s workspace; the difference from a platform admin who can see every customer is covered in SaaS user accounts and permissions.

ActionOwnerAdminMemberViewerScopeWho may grant
View recordsyesyesyesyesown workspaceowner, admin
Create a recordyesyesyesnoown workspaceowner, admin
Edit a recordyesyesown records onlynoown workspaceowner, admin
Delete a recordyesyesown records onlynoown workspaceowner, admin
Export all recordsyesyesnonoown workspaceowner
Invite a useryesyesnonoown workspaceowner
Remove a useryesyes, not ownersnonoown workspaceowner
Change a user’s roleyesmember and viewer onlynonoown workspaceowner
Reset another user’s password (send a reset link)yesyesnonoown workspaceowner
Create or revoke API keysyesyesnonoown workspaceowner
View and change billingyesnononoown workspacenobody: owner role only
Transfer ownershipyesnononoown workspacenobody: owner role only
Delete the workspaceyesnononoown workspacenobody: owner role only

The reset row sends a link; nobody sets a password for someone else. The rules for the new password itself are a separate question: NIST minimum password length. Look at the “change a user’s role” row: an admin who could set anyone to admin, or to owner, could grant themselves upward through a friend’s account, so the cell is limited to member and viewer.

Matrix B: a two-sided marketplace

Buyer, seller, support agent and platform admin. Here the platform’s own staff sit in the same matrix as the customers, so their powers get written down too.

ActionBuyerSellerSupport agentPlatform adminScopeWho may grant
Browse listingsyesyesyesyesall public listingseveryone has it
Place an orderyesnononoown accountsign-up
View an orderown ordersorders for own listingsany orderany orderas listedplatform admin
Cancel an orderown, before it shipsown listingsyesyesas listedplatform admin
Create or edit a listingnoown listingsnoremove onlyown listingsplatform admin (seller approval)
Set payout bank detailsnoown accountnonoown accountnobody: seller only
Refund an ordernonoup to a limit the business setsyesany orderplatform admin
Read buyer and seller messagesown threadsown threadson an open ticketyesas listedplatform admin
Impersonate a usernonowith a logged reasonwith a logged reasonany accountanother platform admin
Approve a sellernononoyesany accountanother platform admin
Suspend a usernononoyesany accountanother platform admin
Change a user’s rolenononoyesany accountanother platform admin
Export all usersnononoyesallanother platform admin

Support can view an order and cannot refund above the limit; impersonation is its own row because “support can see everything” hides the one action that turns a support session into the customer’s session.

Matrix C: an AI product with an API

Free, paid, team admin, internal staff, and one principal that is not a person: an API key limited to reading. A key is a role too, and the way keys and service callers are identified belongs to API authentication best practices.

ActionFreePaidTeam adminInternal staffAPI key (read-only)ScopeWho may grant
Run a generationwithin the free limityesyesnonoown account or teambilling plan
View own historyyesyesyesnoyesown account or teambilling plan
View the team’s shared projectsnoown teamown teamon a ticket, loggedown teamown teamteam admin
Upload files to a knowledge basenoyesyesnonoown teamteam admin
Delete own datayesyesyesnonoown accounteveryone has it
Invite teammatesnonoyesnonoown teamteam admin
Change a teammate’s rolenonoyes, never to staffnonoown teamteam admin
Manage billing and seatsnoown accountyesnonoown teamteam admin
Create or revoke API keysnoyesyesnonoown teamteam admin
Write or delete through the APInoyesyesnonoown teamteam admin
Change the team’s model settingsnonoyesnonoown teamteam admin
View any customer’s promptsnononoon a ticket, loggednoallstaff lead
Change a customer’s plan or creditsnononoyesnoallstaff lead

Fill the key column on purpose: a key minted for a dashboard that only reads should not be able to write, and this column is where that decision gets written down.

What is role-based access control

Role-based access control gives permissions to roles and roles to users, so every check asks what the role may do, never who the person is. A small SaaS can run it with a handful of roles, one permission map and one function that every route calls.

NIST’s project page says the model was “formalized in 1992 by David Ferraiolo and Rick Kuhn”, adopted as ANSI/INCITS 359-2004 and “revised as INCITS 359-2012 in 2012”; NIST’s role-based access control project keeps the history.

User-based access control is the thing before roles: permissions attached to each person one by one. An app has it when a check reads user.email === "founder@...", and it breaks the day that person leaves. Group-based access control gives permissions to a group of people; in a small SaaS a group and a role collapse into the same thing until teams inside one customer need different data (my reading).

Role-based access control software, meaning a library or a policy engine, earns its place when the roles no longer fit on one page or the rules need attributes. Below that, my working rule is one can(role, action) function reading one constant, and it is the whole system:

const PERMISSIONS = {
  owner: ['record.read', 'record.write', 'user.invite', 'user.set_role', 'billing.manage', 'workspace.delete'],
  admin: ['record.read', 'record.write', 'user.invite', 'user.set_role'],
  member: ['record.read', 'record.write'],
  viewer: ['record.read'],
} as const;

type Role = keyof typeof PERMISSIONS;
type Action = (typeof PERMISSIONS)[Role][number];

export function can(role: Role, action: Action): boolean {
  const allowed: readonly string[] = PERMISSIONS[role] ?? [];
  return allowed.includes(action);
}

An unknown role gets an empty list, so the default is deny. Every route calls can() with the role it read from the server, and the matrix and this constant say the same thing.

What goes wrong without it

SymptomCauseWhere the matrix fixes it
Support, sales and the code each give a different answer to “can a member export?”Nobody wrote the rule downOne row per action, one answer per role
The button is hidden for viewers, but the API still accepts the callThe check lives in the UI onlyRows are API actions, so every deny cell becomes a request to test
One route checks "admin", another checks "Admin"Role names are strings spread through the codeOne permission map the code reads
A member promotes themselves, or an admin invites an ownerNo limit on who may grant which roleThe “who may grant” column
One person can add a payout account and approve the payoutTwo harmless permissions combineA marked pair that needs a second person

The first row is the plain case: it is unclear who can do what in the app, and each person you ask gives a different answer. The second row is broken access control at its plainest, and it is why the rows are API calls.

Picture a codebase where the role is compared as a string in many files, and one file spells it with a capital letter. Nothing fails in a demo; the one screen that uses that file refuses its own admins, or lets everyone in, depending on which way the comparison was written.

The fourth row is the one I would look for first. In my June and July 2026 audits, one food-delivery app kept the account type (customer, seller, driver, admin) in a role column the account’s owner could edit, so users could promote themselves; the full story is in the role column a signup could set for itself. My take on the lesson: a matrix with no “who may grant” column cannot even show a role that promotes itself, so nobody thinks to test it.

The last row is about toxic combinations: two permissions that are fine apart and dangerous together, such as creating a payout destination and approving a payout, or editing a price and issuing a refund. Segregation of duties, “SoD” in audit language, is the control. At four people my working rule is a second person or a logged approval for those few pairs, not an enterprise tool.

Seven of the 21 third-party apps had confirmed cross-user or cross-tenant authorization failures, where a logged-in user could read or write another customer’s data. All 21 were apps I audited in June and July 2026, a selected set, so the count says what I found in them and nothing about how often AI-built apps fail in general. The half of that problem about which rows a user may see belongs to multi-tenant data isolation; the matrix covers which actions.

Access control models: DAC, MAC, RBAC and ABAC, and which one a small SaaS needs

The 4 access control models differ in who decides. In DAC the owner of a resource decides, in MAC a central policy decides and owners cannot override it, in RBAC the role decides, and in ABAC a rule over attributes decides. A small SaaS runs RBAC plus a few attribute rules: tenant, ownership, plan.

These four names turn up on a customer’s security review form and in NIST’s documents, so it helps to know which one your matrix is. For a small product my answer is usually “RBAC with a scope column”, and the table shows why.

ModelWho decidesThe rule looks likeWhere you already use it (my reading)
DAC (discretionary)The object’s owner, or anyone authorized to control its access, for part of the access control”Share this document with a teammate”File permissions; sharing a doc in your own app
MAC (mandatory)A central authority, not the individual owner of an object”Only users cleared for this label may read it”Operating systems running SELinux; classified systems
RBAC (role-based)The role: permitted actions are tied to roles, not to individual people”Admins may invite users”The matrices above
ABAC (attribute-based)A policy evaluated over attributes of the subject, the object, the operation and sometimes the environment”A member may edit a record if it belongs to their tenant and is not locked”The scope column in your matrix

Mandatory access control in cyber security is that central-authority rule applied to data and systems: a label on the information, a clearance on the person, and a policy the owner cannot change. For a small SaaS, knowing what mandatory access control is matters mostly as vocabulary, because customers who share their own documents need the owner discretion that MAC takes away. Mandatory access control in a DBMS would mean labels on rows and clearances on database users; NIST’s glossary does not name a database form, and the everyday cousin, Postgres row-level security, is in the MAC section below. M access control and MAC name the same model: the M stands for mandatory.

RBAC vs ABAC is the choice the scope column settles. NIST SP 800-162 defines ABAC as authorization “determined by evaluating attributes associated with the subject, object, requested operations, and, in some cases, environment conditions against policy, rules, or relationships”. My working rule for a small SaaS: start with roles, and add the few attribute rules you truly have (tenant, ownership, plan, record state) as the scope column. Pure ABAC is rarely needed at this size and harder to review, because a reviewer cannot read the answer off a table.

When a customer’s question says “NIST access control”, it points at the AC family of NIST SP 800-53 Rev. 5, where a reviewer’s vocabulary comes from. Three controls map straight onto this page: AC-2 ACCOUNT MANAGEMENT is the matrix plus the access review, AC-5 SEPARATION OF DUTIES is the toxic-pair rule, and AC-6 LEAST PRIVILEGE is the next section.

What is an example of mandatory access control (MAC)?

Mandatory access control (MAC) examples come from operating systems and classified data: a label on each file and each user, and a central policy the owner cannot change. On Linux, SELinux is one. Inside a SaaS, Postgres row-level security is the nearest everyday cousin, without the labels.

The classic case is classification: NIST’s glossary entry for mandatory access control describes access based on “the sensitivity (as represented by a label) of the information” and “the formal authorization (i.e., clearance) of users”, with decisions “made by a central authority, not by the individual owner of an object”. In an operating system, the SELinux Notebook puts it this way: with SELinux enabled, “the subject (and therefore the user) cannot decide to bypass the policy rules being enforced by the MAC policy”.

The cost, in my reading, is that every change goes through whoever owns the central policy. That is the disadvantage that matters here, and it is why a SaaS whose customers share their own documents does not run pure MAC.

The nearest thing a small SaaS already runs is Postgres row-level security. Supabase’s row-level security guide says each policy “is attached to a table” and “executed every time a table is accessed”, like “adding a WHERE clause to every query”. So a policy on the table, not the row’s owner, decides which rows a query sees. That makes RLS policy-enforced like MAC, but it is not a labeled multilevel system, and calling it MAC outright would overstate it (my reading).

One homonym: “MAC access control” on a home router means MAC address filtering, a list of allowed network card addresses, which has nothing to do with this topic.

Access control lists and roles: ACL in cyber security

An access control list is a list attached to one resource naming who may do what to it, where RBAC attaches permissions to roles instead. Document sharing inside an app is an ACL, and the role matrix says which roles may edit those lists.

NIST’s definition of an access control list is “A list of entities, together with their access rights, that are authorized to have access to a resource.” That is the difference between ACL and RBAC: the ACL lives on the resource and names principals, while RBAC lives on the role and names actions.

In networking, an ACL is something else: the allow and deny list on a router or firewall that filters traffic by address and port.

Inside a SaaS, “these three people can view this file” is an ACL sitting beside the role model. The matrix then needs one row for it, such as “change who a document is shared with”, and a yes or no per role. One question, two vocabularies.

Least privilege in a four-person company: the smallest role that still lets people work

The principle of least privilege means each person and each key gets the smallest set of permissions that still lets the work happen, and nothing by default. In a four-person company my working rule is a named role per person, two owners, and elevated access that expires.

NIST’s definition of least privilege reads: “A security principle that a system should restrict the access privileges of users (or processes acting on behalf of users) to the minimum necessary to accomplish assigned tasks.” In the principle of least privilege (PoLP), “minimum necessary” is the phrase that matters: every permission a person holds should have a task that uses it.

Here is a principle of least privilege example for a founder, an engineer, a support person and a contractor:

PersonWhat they need weeklyThe roleWhat is left out
FounderBilling, customer escalations, the odd data fixApp owner; provider ownerDay-to-day production database writes
EngineerDeploys, logs, staging dataApp admin; provider developer roleBilling; exporting all users
Support personReading orders, resending emails, account lookupsSupport role in the appExporting all users; refunds above the limit
ContractorThe feature they were hired forThe staging project onlyThe production database, production secrets, billing

My permission best practices for a team this size, as working rules:

  • Default deny: a new route or action is refused until a matrix row says otherwise.
  • New roles start empty and gain permissions one row at a time.
  • No shared logins, on the app or on any provider console.
  • Elevated access is granted for a task and expires when the task ends.
  • The owner role is held by two people, never one, and never the whole team (my working rule).
  • Every grant is logged; what to record and for how long is in audit logging best practices.

People change jobs, contractors finish and roles drift, so least privilege slowly erodes after launch. The access review section below is how it gets restored.

Least privilege on the provider accounts: AWS IAM groups, the GCP Viewer role and read-only access

Least privilege on provider accounts means most of the team holds a read-only role and, by my working rule, two people hold the one that can delete things. On AWS that is an IAM user group with a read-only policy; the other consoles have their own read-only role, some only on certain paid plans.

This is the same control applied to your own team instead of your app’s users. IAM stands for identity and access management, and AWS names its service AWS Identity and Access Management. The table below gives six examples of IAM, one per provider, with role names and plan conditions as each provider’s docs stated them on September 27, 2026.

ProviderThe read-only roleThe role that can destroy thingsPlan the read-only role needsHow many should hold the destructive one (my working rule)
AWSAn IAM user group with the ReadOnlyAccess managed policyA user group with administrator permissionsNot stated in AWS’s docsTwo
Google CloudViewer (basic role)Owner (basic role)Not stated in Google Cloud’s docsTwo
SupabaseRead-OnlyOwner; AdministratorTeam or Enterprise planTwo
VercelPro Viewer; Enterprise ViewerOwnerPro Viewer on Pro, Enterprise Viewer on EnterpriseTwo (Vercel recommends at least two owners)
GitHubRead (organization repository role)Organization owner; repository AdminAn organization: private repositories on a personal account cannot give collaborators read-only accessTwo (GitHub says no fewer than two owners)
StripeView OnlySuper Administrator; AdministratorNot stated in Stripe’s docsTwo Super Administrators; Stripe allows only one account Owner

On AWS, AWS’s IAM user groups page says “An IAM user group is a collection of IAM users”, that you attach an identity-based policy to it, and that you “cannot identify a user group as a Principal in a policy” because “groups relate to permissions, not authentication”. The ReadOnlyAccess managed policy is described as “Provides read-only access to AWS services and resources.” Read-only still means data: its policy document includes actions such as s3:Get* and dynamodb:Scan, so a read-only user can read your stored objects and table items. My working rule for AWS IAM groups on a small team is two groups: read-only for everyone, and admin behind MFA for the two people who need it.

For the GCP Viewer role, Google Cloud’s roles overview describes “Permissions for read-only actions that don’t affect state”, and adds a warning in its own words: “In production environments, do not grant basic roles unless there is no alternative. Instead, grant the most limited predefined roles or custom roles that meet your needs.”

The platforms most builder apps actually run on have their own role lists. Supabase’s organization roles are Owner, Administrator, Developer and Read-Only, and “Read-Only role is only available on the Team and Enterprise plans”; a footnote on the same page adds that the Read-Only role “is able to access secrets”. Vercel’s team roles are “available on Enterprise and Pro plans”, with Pro Viewer on Pro. GitHub’s organization roles say the owner role “should be limited, but to no less than two people”, and GitHub’s personal-account docs say “Collaborators can’t have read-only access to repositories owned by a personal account.” Stripe’s team roles include View Only, for people who “need to view payments, balance, and connected accounts, but can’t edit any of them”.

Who owns each of these accounts at all is a separate question, answered in who owns the hosting account. The second factor on the consoles is in two-factor authentication for owner and admin accounts.

How to document user roles and permissions

Documenting user roles and permissions takes 6 steps: list the actions from the code, list the roles that exist, fill each cell, add scope and who may grant, keep the page in the repository, and make the code read from one permission map.

  1. 01 List the actions from the code, not the UI: every route, server action, RPC and storage bucket, one row each, grouped by the noun they act on.
  2. 02 List the roles that exist in the database today, including the accidental ones, such as a role value only one test account has.
  3. 03 Fill each cell from what the business wants, then mark every cell where the code currently disagrees.
  4. 04 Add the scope column (own record, own tenant, all) and the who-may-grant column.
  5. 05 Keep the matrix as one page in the repository next to the code, versioned with it and linked from the README.
  6. 06 Point the code at it: one permission map that every check reads, so the document and the behavior cannot drift apart silently.

Step one is where the real list comes from. Search the route folder, the server actions and the database functions, not the navigation menu, because a single settings screen can hide several writes: rename the workspace, invite, change a role, delete.

Step three is where the useful arguments happen. When the business says “support can refund” and the code lets support refund any amount, that cell gets a note, and the fix is either a code change or a matrix change. Both are fine; leaving them different is the bug.

Where the role itself is stored, and why it must never sit in a field the user can write, belongs to the admin panel security checklist. The site’s articles on Supabase row-level security and on Firebase security rules already walk through a user editing their own role, so this page does not repeat them.

An access control policy template a customer will accept

An access control policy for a small company fits on one page with nine lines: what it covers, the roles, how access is requested, least privilege, staff sign-in rules, joiners and leavers, the review cadence, logging, and the owner with a date.

Each line is one sentence the company can stand behind today. Fill in the brackets and delete anything you do not actually do.

ACCESS CONTROL POLICY

1. Purpose and coverage: This policy covers access to [the product], its production database, and the provider accounts listed in [the inventory].
2. Roles: Roles and their permitted actions are defined in the role matrix at [repository path]; this policy does not copy it.
3. Requesting access: Access is requested in [channel] and approved by [role or name] before it is granted.
4. Least privilege: Every person and key gets the smallest role that lets them work; anything not granted is denied.
5. Staff sign-in: Staff sign-in requirements are set in [authentication policy or section].
6. Joiners, movers, leavers: Access is granted on start, changed on a role change, and removed within [time the company chooses] of leaving.
7. Review: Every account on every system is reviewed [cadence the company chooses], and the decisions are kept.
8. Logging: Grants, role changes and admin actions are logged and kept for [period].
9. Owner: [Name, role] owns this policy. Last reviewed: [date].

Keep what is required apart from what is good practice. A customer’s questionnaire asks whether a policy like this exists and whether you follow it; answering those rows belongs to the security questionnaire itself, and the buyer’s own control list to the MVSP checklist. This template is not legal advice and not a certification.

User access review: the quarterly pass, with a template

A user access review is a dated pass over every account on every system asking whether the person still needs the role. It has 5 steps: export, match accounts to people, flag leavers and excess roles, remove or downgrade, record the decision. Admin roles get the pass more often.

In practice the user access review process starts with the exports: the app’s own admin table first, then every provider in the table above. Match each account to a person who works with you now. Flag leavers, shared logins, accounts nobody has used in months, and anything above the person’s role in the matrix. Remove or downgrade what you flagged, then write down each decision and who made it.

The privileged access review process is the same pass limited to owner and admin roles and to anyone with production data access, run more often than the full one. My working rule for a small team is a full review about every three months, with the privileged pass more often than that. NIST SP 800-53 AC-2 asks organizations to “Review accounts for compliance with account management requirements [Assignment: organization-defined frequency]”, so the frequency is yours to set and write into the policy.

Use the table below as your user access review template, and keep its columns fixed so the access review process runs the same way each time.

SystemAccountPersonRoleStill needed?Last usedDecisionReviewerDate
The appsupport@Support personSupportyesthis weekkeepFounder(date)
Supabasecontractor’s loginContractor (finished)Developernolast monthremoveEngineer(date)
GitHubshared bot accountNobody namedWritenounknownreplace with a named keyFounder(date)
Stripeengineer’s loginEngineerAdministratorread-only would dothis weekdowngrade to View OnlyFounder(date)

Under the table sits the checklist: eight questions about roles and permissions that a user access review should ask before anyone signs it.

  • New roles since the last review, and whether each is in the matrix.
  • Cells in the matrix that changed, and whether the code changed with them.
  • Toxic pairs: anyone holding both halves of a marked pair.
  • API keys and service accounts, with an owner named for each.
  • Former contractors and anyone who left since the last review.
  • The staging project, which collects old logins because nobody watches it.
  • Support impersonation: who used it, and whether each use had a ticket.
  • The evidence: exports, decisions and the date, filed where you can find them.

If a customer’s audit asks for your user access review evidence, send the filed tables with their dates. A spreadsheet is enough while every export can be pulled from each dashboard; an access review tool earns its cost once they cannot (my working rule). The record of what changed between reviews comes from the audit log linked in the least-privilege section.

How to verify it

A role matrix is verified by calling the API directly as each role and checking what comes back against the matrix, cell by cell, which compares the role matrix to the actual code rather than to the screens. Allowed cells must work and refused cells must return no data and change nothing. The deny cells are where the failures hide.

Run this against a staging copy, never production:

  1. 01 Create one test account per role, plus one account in a second tenant.
  2. 02 Turn the matrix into an expected-results table: for each action, the request and, per role, allowed or refused. Write down what a refusal looks like on your stack before you run anything.
  3. 03 Call the API directly as each role, never through the UI, each with that role’s own session, and include every deny cell.
  4. 04 After each write, allowed or refused, read the record back as its owner to see what actually changed.
  5. 05 Diff observed against expected: every mismatch is either a code bug or a matrix error, and both get fixed.
  6. 06 Keep the run as an automated test so it repeats on every change.

A refusal is whatever your stack returns when it refuses, so step two matters. A server route that checks the role answers with the error status your app chose, such as a forbidden or a not-found response. On Supabase, the row-level security guide linked in the MAC section says a using clause “filtering the row out raises nothing, matches zero rows”, while a missing grant or a with check violation raises error 42501. So a filtered read comes back empty with no error, a filtered update or delete changes no rows, and a blocked insert returns an error. That is why step four exists: the same guide warns that “Matching no rows is not proof on its own.” Test with each role’s own session, never with the service_role key or a secret key: the guide says the service_role role “bypasses RLS”, and a secret key “bypasses RLS only when the request carries no user access token”, so either can make a refused cell look allowed.

Here is the expected-results table for part of Matrix A:

ActionRequestOwnerAdminMemberViewerA correct refusal looks like
Invite a userPOST /api/invitesallowedallowedrefusedrefusedThe app’s chosen error status; no invite exists on read-back
Set a user to adminPATCH /api/members/:id with role: "admin"allowedrefusedrefusedrefusedError status; the role is unchanged on read-back
Export all recordsGET /api/exportallowedallowedrefusedrefusedError status; no file
Delete the workspaceDELETE /api/workspaces/:idallowedrefusedrefusedrefusedThe owner can still read the workspace
Edit a record another member createdSupabase update on recordsallowedallowedrefusedrefusedNo error, zero rows matched; the row is intact on read-back
Create a recordSupabase insert on recordsallowedallowedallowedrefusedError 42501
Read a record in the second tenantSupabase select on recordsrefusedrefusedrefusedrefusedEmpty result, no error

The browser-only first rung, in Chrome, needs no test code. Sign in as an admin in one browser profile and as the lowest role in another. In the admin window, open DevTools, go to the Network panel, trigger an admin-only action, right-click the request and choose Copy, then Copy as cURL. Chrome’s DevTools documentation describes that option as copying “the request as a cURL command” without listing the headers it carries, so check the copied command for a Cookie header or an Authorization bearer token. Replace that credential with the low role’s, copied from the low role’s own window, run the command in a terminal, and expect a refusal as you defined it in step two.

Keep three things as evidence: the matrix version (its commit), the expected-results table with each response filled in, and the date of the run. Then do one spot check the other way: search the code for role strings and confirm each one appears in the matrix, because a role the matrix does not know about is a role nobody tested.

In the Production Hardening Sprint, deliverable 1.7 is verified this way: compare the role matrix with tested backend permissions.

Where the sprint does this

Deliverable 1.7 writes a one-page matrix defining roles and their permitted actions. Deliverable 1.3 enforces permissions on every protected route, API endpoint and server action, and deliverable 1.5 restricts admin routes and operations to explicit, verified roles. The production readiness report, deliverable 13.1, accounts for all 123 IDs, keeps failures visible until resolved and explains genuine non-applicable items. New features that change the product’s core capabilities are separate work; the sprint includes only the supporting interfaces the listed controls need, such as session management, account deletion and billing self-service. Formal third-party certifications and independent audit opinions are separate from the sprint deliverables. The full list is in the published scope.

Common questions about roles and access control

What are the three primary rules for RBAC?

The three primary rules for RBAC, in the 1992 paper that formalized the model, are role assignment (a subject can run a transaction only if it has a role), role authorization (a subject’s active role must be authorized for that subject) and transaction authorization (a transaction runs only if it is authorized for the subject’s active role). Other summaries call the third one permission authorization.

The source is the 1992 paper that formalized RBAC, which also sketches an optional fourth rule “to enforce control over the modes in which users can access objects through transaction programs”.

Does AWS use RBAC or ABAC?

AWS uses both. Its IAM documentation calls role-based access control “The traditional authorization model used in IAM”, implemented by writing policies per job function, and it supports attribute-based access control through tags: “AWS calls these attributes tags.”

AWS also notes that an RBAC role in this sense “is distinct from an IAM role”. The details are on AWS’s page on attribute-based access control.

What are the differences between LDAP and RBAC?

LDAP is a protocol and RBAC is an access control model. LDAP “provides access to distributed directory services”, in the words of its specification, while RBAC decides what each role may do. In my reading they meet in practice: a company directory reached over LDAP can hold the groups that a role model maps to roles.

The protocol itself is defined in RFC 4511.

What is the difference between roles and groups?

A group collects people and a role collects permissions. NIST describes a role as “a collection of access authorizations that a user receives” by taking on that role, while a group is simply a set of users you manage together. On AWS the split is explicit: a user group exists to give several IAM users the same permissions, and a policy cannot name the group as its principal.

What are the disadvantages of RBAC?

RBAC has three weaknesses, in my reading: roles multiply until nobody remembers what each one does, a role alone cannot express context such as “only records in my tenant” or “only while the order is open”, and assignments go stale when people change jobs. The scope column answers the context problem, and a regular access review answers the stale assignments.

What is a violation of the principle of least privilege?

A violation is any access larger than the work needs. Three examples: one owner login shared by the whole team, a contractor who still has production database access after the job, and an API key with write rights used only to read a dashboard.