Most access control conversations start and end with role names. Someone proposes an “admin” role, a “viewer” role, maybe an “analyst” role, and the team moves on. That naming exercise feels like RBAC, but it is only the label on the outside of the box. The real decisions live inside a structure that almost nobody draws on the whiteboard. That structure is the permission matrix, and its anatomy determines whether your access control is precise or just a set of suggestive words.
This article opens up that matrix. It uses PMAP’s authorization model as a concrete, real-world example, where a role is defined by 10 entity types crossed against 6 actions, producing 60 possible entity-action pairs. We will walk the grid cell by cell, then look at how scope sits on top of it so the same matrix can mean different things for different tenants. If you want the operational walkthrough of building roles and issuing grants, that lives in a separate guide on RBAC roles and scoped grants. This piece is about the design itself, the shape of the thing, and why that shape matters.
The wider context for this topic is multi-tenant security, where one platform serves many isolated customers. If you are arriving here from the broader subject of multi-tenant vulnerability management, the permission matrix is the layer that makes per-tenant precision possible in the first place.
Why Roles Alone Are Not Enough
A role name is a promise. “Analyst” promises that a person can do analyst things. The problem is that the name carries no enforceable meaning until someone defines exactly what an analyst is permitted to touch. Two organizations can both create an “analyst” role and mean wildly different things by it. Without a structured definition, the role is a label waiting for an argument.
Real RBAC splits this single fuzzy idea into three separate concerns, and keeping them separate is what makes the system both auditable and flexible.
The first concern is the role definition itself: which entity-actions a role grants. This is the permission matrix. It answers the question “what can this role do, in principle?”
The second concern is the user-access assignment: which user holds which role, at what scope, and for how long. This answers “who has this role, and where does it apply?” A role definition is reusable. The assignment is where it gets attached to a person and bounded to a slice of the platform.
The third concern is scope resolution: at request time, the system builds a complete picture of a user’s effective access by combining their grants. This answers “given everything this user holds, what may they do right now?”
PMAP treats these as three distinct layers. The matrix defines capability. The grant binds capability to a user and a tenant boundary. Scope resolution evaluates the combination on every authenticated request. When people say RBAC is hard, they usually mean they collapsed these three concerns into one and then could not explain why a particular user could see a particular record. Separating them is the first design decision, and the matrix is the foundation the other two layers stand on.
Anatomy of a Permission Matrix
A permission matrix is a grid. One axis lists the things you can act on. The other axis lists the things you can do. Every cell where the two axes cross is a discrete, individually assignable permission. That is the entire idea, and its power comes from how granular it forces you to be.
In PMAP, a role’s permissions are expressed internally as a map from each entity type to the list of actions allowed on it. The structure looks like this:
PermissionMatrix = map[entity_type][]action
// e.g. {"finding": ["view","create","approve"], "report": ["view","export"]}
Read that example out loud and the precision becomes obvious. This role can view, create, and approve findings. It can view and export reports. It cannot delete anything. It cannot touch integrations or scans at all, because those entities do not appear in the map. There is no ambiguity to argue about later. The role is exactly the sum of the cells that were checked.
This is the difference between a role that is a name and a role that is a specification. The matrix turns a vague title into a list of concrete capabilities that an auditor can read, a developer can enforce, and a customer can trust.
The Ten Entity Types
The entity axis defines the kinds of objects a permission can govern. PMAP supports ten canonical entity types, and the list maps directly onto the real objects a security platform manages:
- company is the tenant record itself
- asset covers the hosts, services, and resources under management
- project marks engagement or scoping boundaries within a company
- finding is the individual vulnerability record that flows through triage
- report covers generated outputs and exports
- runbook is an automation playbook
- rule is the policy logic that governs findings
- integration is a connection to scanners and external systems
- scan covers scan jobs and their results
- user is an account and access record
The choice of entities is not arbitrary. Each one corresponds to a unit of work or a unit of risk that someone might reasonably need to grant or withhold independently. A consultant might need to view findings but never touch integrations. A reporting analyst might need export rights on reports but no rights on scans. Because each object type is its own row in the matrix, those distinctions are expressible rather than aspirational.
The Six Actions
The action axis defines what can be done to an entity. PMAP defines six canonical actions:
- view is read access
- create brings a new instance into existence
- update modifies an existing instance
- delete removes an instance
- approve signs off on a gated change
- export extracts data out of the platform
Five of these are the familiar lifecycle verbs. The sixth, approve, is worth pausing on, because its presence reveals how the matrix accommodates governance rather than just CRUD. Approval is its own action because the right to approve something is genuinely separate from the right to change it. A person who can update a finding should not automatically be the person who can approve a sensitive change to it. By making approval a first-class action in the matrix, the model lets you grant the power to sign off without granting the power to edit, which is exactly what separation of duty requires. Likewise, export is split out from view because the ability to read a record on screen is a different risk from the ability to pull data out of the platform in bulk.
Sixty Possible Entity-Action Pairs
Multiply the two axes and you get the full canvas: 10 entity types crossed with 6 actions, for 60 possible entity-action pairs. Any combination of an entity and an action can be assigned to a role independently of any other combination. The matrix does not bundle. Granting finding:view does not drag along finding:update. Granting report:export says nothing about scan:export.
Sixty cells is a meaningful number to sit with. It is large enough to express genuinely fine-grained policy. A role can be exactly “view and approve findings, view and export reports, view assets” and nothing else, which is four checked cells out of sixty. At the same time, the grid stays small enough to render as a single screen of checkboxes a human can scan and reason about. That balance is deliberate. A matrix that is too coarse forces over-granting, where people get powers they do not need because the role could not be drawn more tightly. A matrix that is too sprawling becomes unmanageable, and administrators stop reasoning about it and start copying whatever the last role had. Ten by six lands in the zone where the matrix is both expressive and legible.
This granularity is also what NIST’s RBAC model, standardized as INCITS 359, points toward when it describes permissions as associations between operations and protected objects. The matrix is simply that association made concrete and assignable.
How a Role Becomes a Matrix
A role is not stored as a single blob of permissions. In PMAP the permission data lives in a dedicated relationship: each checked cell is a row pairing a role with one entity_type and one action. When the system loads a role, it assembles those rows back into the map structure shown earlier, grouping every action under its entity.
This row-per-cell storage has a quiet benefit. Because each permission is an individual record rather than a packed field, the system can answer questions about permissions with ordinary set operations rather than bespoke parsing. Adding a capability to a role is adding a row. Removing one is dropping a row. The matrix you see in the role builder is a faithful projection of those rows, and the role builder itself is populated from a catalog endpoint that returns the full list of entity types and actions, so the UI never hardcodes the grid. When the canonical list changes, the builder reflects it without a frontend change.
System roles add one structural rule worth noting. A role flagged as a system role is seeded by database migration and cannot be modified or deleted through the API. The platform ships one such role, platform_admin, and its protection is structural rather than conventional. Even a user who creates their own role and names it platform_admin does not inherit special authority, because the system checks both the immutable system flag and the role name before granting elevated access. The label alone earns nothing. We will return to what that role actually does shortly.
Scope on Top of the Matrix
Here is where many access models stop short, and where the matrix alone would leave a dangerous gap. The matrix says what a role can do. It says nothing about where. In a single-tenant system that omission is harmless. In a multi-tenant platform it is the difference between safety and a data breach.
PMAP layers scope on top of every grant. When you assign a role to a user, you also choose a scope, and there are three scope types:
| Scope type | Meaning | |—|—| | global | Unrestricted. The user can act across all tenants. | | company | The user can act on the specified company and all of its projects. | | project | The user can act on only the specified project. |
The same matrix means three different things depending on which scope it is bound to. A role that grants finding:view and finding:update, attached at company scope, lets the holder work findings across one entire customer. The identical role attached at project scope confines that same capability to a single engagement. The matrix is the verb. The scope is the boundary. You compose them.
This composition is what keeps role definitions reusable. You do not need a separate “Acme analyst” role and “Globex analyst” role. You define “analyst” once as a matrix, then grant it at company scope to the right person for Acme and at company scope to a different person for Globex. The capability is shared. The boundary is per-grant.
Why Project Scope Does Not Open the Company
The most important design decision in the scope layer is also the least intuitive, and it is worth stating plainly because getting it wrong is a classic multi-tenant leak. A project-scoped grant does not implicitly open the company that owns the project.
It would be tempting to reason that since a project belongs to a company, a project consultant should naturally be able to see the company too. PMAP refuses that inference on purpose. A project-scoped grant populates only the allowed-project set. It does not populate the allowed-company set. If it did, a consultant brought in for one engagement would be able to see every sibling project at the same company, which is a cross-engagement data leak.
Consider an MSSP running two separate penetration tests for the same client, handled by two separate outside consultants who must not see each other’s work. Both projects sit under one company record. If project scope quietly granted company access, each consultant would inherit visibility into the other’s engagement. By keeping project scope strictly project-only, the platform makes the narrow grant actually narrow. The boundary you drew is the boundary you get. This is the kind of constraint that looks like a small implementation detail and is in fact the whole point of the scope layer.
Time-Bound Grants and Expiry
Scope answers where. A grant can also answer for how long. Every assignment can carry an optional expiry timestamp, which turns a standing permission into a time-bound one. This matters for the same reasons that temporary access matters everywhere in security. The contractor who needs three weeks of access should have three weeks of access, not indefinite access that someone has to remember to clean up.
PMAP handles expiry without forcing administrators to babysit it. When a grant carries an expiry, it is excluded from the user’s effective access the moment it lapses. The design goes one step further to make revocation prompt rather than lazy. The scope cache, which we cover next, has its lifetime capped to the soonest upcoming expiry across a user’s grants, so a grant that expires at a specific minute stops mattering within seconds of that minute rather than lingering until an unrelated cache window happens to roll over. A background worker also sweeps expired grants, emits audit events recording the expiry, and marks each row so the event is never duplicated. Time-bound access lapses on schedule, leaves a record, and needs no manual cleanup.
How the Matrix Is Resolved Per Request
A defined matrix and a stack of grants are static facts. The interesting work happens at request time, when the system has to turn all of that into a single yes-or-no decision for a specific user touching a specific record. PMAP does this through scope resolution, which runs as middleware ahead of every authenticated request.
On each request, the authorization layer resolves the user’s complete scope context before any handler touches the database. That context bundles the resolved permission matrix together with the set of companies and projects the user is allowed to reach, plus the flags that mark a platform administrator or an unrestricted global grant. Every downstream filter then reads from that one resolved context, so authorization is decided once and applied consistently rather than re-derived in scattered places.
Resolving this on every single request would be wasteful if it meant rebuilding everything from scratch each time, so the resolved context is cached with a 30-second time-to-live. The cache is not the source of truth, though. It is an optimization with a safety net. On any mutation that changes a user’s access, the cache is explicitly invalidated rather than left to expire. The 30-second window only ever covers out-of-band changes, such as a direct database edit that bypasses the API. In normal operation, a change to access takes effect immediately because the relevant cache entry is purged the moment the change lands.
The invalidation rules are themselves a small matrix worth knowing, because they reflect how widely a change ripples:
| Event | Cache invalidated for | |—|—| | Granting access to a user | That user only | | Revoking a user’s access | That user only | | Changing a role’s permissions | All users | | Creating a role with permissions | All users | | Deleting a role | All users |
The logic is intuitive once you see it. Granting or revoking a single user’s access can only affect that user, so only their cache is purged. But editing a role’s matrix changes the meaning of that role for everyone who holds it, so every cached scope is invalidated. The blast radius of the invalidation matches the blast radius of the change.
The Platform Admin Short-Circuit
Not every authority decision needs to walk the matrix. The platform_admin system role holds wildcard authority, and scope resolution recognizes this and short-circuits. Rather than loading and unioning a full permission matrix, resolution stamps the context as unrestricted and platform-administrative and returns immediately. When the effective permission matrix is requested for such a user, it returns the wildcard form {"*": ["*"]}, meaning every action on every entity.
The short-circuit is an efficiency, but it is also a clarity. The platform administrator is a genuinely distinct kind of actor, the operator of the platform itself rather than a tenant user, and modeling that authority as a special case rather than as a maximally checked matrix keeps the two concepts from blurring. A tenant role, however broad, is still a set of cells. The platform administrator is categorically above the grid. The matrix governs tenant access. The wildcard governs the platform.
Reading Effective Permissions
A single user can hold more than one grant. Someone might have an analyst role at company scope for one customer and a read-only role at project scope for an engagement at another. So the question “what can this person actually do?” cannot be answered by looking at any one grant in isolation. It has to consider all of them together.
PMAP answers this by taking the union of all permissions across every grant a user holds, producing a single combined matrix. If one grant allows finding:view and another allows finding:approve, the effective matrix allows both. The union is permissive in the sense that capabilities add up, but the scope boundaries on each grant still apply independently, so the broadened capability never leaks past the tenant lines drawn on each individual grant. There is a dedicated effective-permissions view precisely so an administrator can debug exactly what a given user’s combined matrix resolves to, which turns “why can this person see that?” from an argument into a lookup.
This union model closes the loop on the whole design. The matrix defines capability per role. Grants bind roles to users and scopes. Resolution composes everything a user holds into one effective answer, evaluated fresh on every request and confined by scope. The 60-cell grid you started with is the atom. Everything else is how those atoms combine into a precise, auditable, multi-tenant authorization decision.
Where to Go From Here
The matrix is the anatomy. Putting it to work is the next step, and PMAP’s identity and RBAC datasheet walks through how this permission model scales from a single role to many tenants without the role definitions multiplying along with the customers. For the broader picture of how authorization fits alongside tenant isolation, asset ownership, and data boundaries, the pillar on multi-tenant vulnerability management sets the full context. And if you simply want the plain-language definition of the model rather than its internals, the primer on what RBAC is is the gentler entry point.
The takeaway is small but load-bearing. Roles are not their names. Roles are their matrices, bounded by scope, composed at request time. Once you see access control as a grid of entity-action cells rather than a list of titles, the whole design stops being mysterious and starts being something you can reason about, audit, and trust.
Frequently Asked Questions
What does a 10×6 permission matrix mean?
It means access is defined by crossing two axes. One axis lists 10 entity types: company, asset, project, finding, report, runbook, rule, integration, scan, and user. The other lists 6 actions: view, create, update, delete, approve, and export. Crossing them yields 60 possible entity-action pairs, and any combination can be assigned to a role independently. A role is the specific set of cells that are checked, which makes its capabilities precise rather than implied by a name.
What are the six supported actions?
The six canonical actions are view, create, update, delete, approve, and export. View, create, update, and delete are the standard lifecycle verbs. Approve is its own action so that the right to sign off on a gated change is separable from the right to make the change. Export is split from view so that pulling data out of the platform is governed separately from reading it on screen.
Does a project-scoped grant give access to the whole company?
No. A project-scoped grant stays strictly project-only. It deliberately does not open the company that owns the project. If it did, a consultant working one engagement could see every sibling project at the same company, which would be a cross-engagement data leak. Keeping project scope narrow is one of the central design choices in the scope layer.
How quickly does a revoked permission take effect?
Effectively immediately. On revocation the affected user’s scope cache is explicitly invalidated rather than left to expire, so the change applies on the very next request. The cache carries a 30-second time-to-live as a safety net for out-of-band changes such as direct database edits, but in normal API-driven operation the explicit invalidation means the revoke does not wait for that window.
How are roles kept reusable across many tenants?
By separating capability from boundary. The permission matrix defines what a role can do. The scope on each grant defines where that role applies. You define a role like “analyst” once, then grant it at company scope to different people for different customers. The capability is shared across tenants while the boundary stays per-grant, so you do not need a separate role definition for every customer.
What is the platform admin short-circuit?
The platform_admin system role carries wildcard authority. When scope resolution encounters such a user, it short-circuits to an unrestricted, platform-administrative context without loading or unioning a full permission matrix, and its effective permissions resolve to the wildcard form {"*": ["*"]}. The role is protected against imitation: both an immutable system flag and the role name are checked, so a user-created role merely named the same earns no elevated authority.
How does the platform decide what a user can actually do on a given request?
It resolves the user’s full scope context as middleware before any handler runs. That context combines the permission matrix from all of the user’s grants, taken as a union, with the set of companies and projects each grant permits. The result is cached for 30 seconds with explicit invalidation on changes, so every downstream filter reads one consistent, freshly accurate answer rather than re-deriving authorization in scattered places.