Vulnerability Management

RBAC Roles and Scoped Grants Without Over-Provisioning

By PMAP Security Team 21 min read

Access control in a vulnerability management platform is rarely the feature anyone gets excited about, yet it is the one that decides whether your security data stays where it belongs. The moment a platform spans multiple tenants, several engagement teams and a rotating cast of external consultants, a single coarse access switch starts to fail in both directions. Turn it up and a project consultant sees data from engagements they were never staffed on. Turn it down and a company analyst spends their week filing tickets to ask for read access they should already have.

PMAP treats this as a configuration problem with a precise answer. Platform administrators compose named roles from a fixed permission matrix, then grant those roles to individual users at exactly one scope: the whole platform, a single company, or a single project. Grants can carry an expiry date so consultant access lapses on its own. Every authenticated request resolves through one authorization layer, so there is no second place where access logic can drift out of sync.

This article is about the operational setup. It walks through how you build a role, how you scope a grant, how time-bound access behaves, and how revocation actually takes effect. If you want the conceptual anatomy of the permission matrix itself, that lives in a dedicated companion piece linked below. Here the goal is practical. You should finish knowing how to provision least-privilege access for a real engagement without over-provisioning anyone.

Why Coarse Access Either Leaks Data or Blocks Work

Most teams arrive at fine-grained access control after living with the alternative. The alternative is usually a small set of broad roles, something like administrator, member and viewer, applied across the entire platform with no notion of which tenant or engagement a user belongs to.

That model breaks the instant your platform holds data for more than one client or business unit. A consultant brought in for a single penetration test gets a member role, and that member role can see findings from every other engagement on the platform. Nothing technically went wrong. The role simply had no way to express the sentence that matters most: this person should see this project and nothing else.

The opposite failure is just as common. To avoid leakage, administrators clamp access down to the point where ordinary work requires a ticket. A company analyst who should be able to read every finding under their own tenant instead waits for someone to grant project-by-project access. Productivity drops, and the access trail becomes a pile of one-off exceptions that no auditor can reconstruct.

PMAP’s documentation frames the design goal directly. Enterprise vulnerability management spans multiple tenants and engagement teams, and without fine-grained, scoped and auditable access control, either security data leaks across engagements or overly restrictive access blocks legitimate work. RBAC is built to dissolve that trade-off. Project consultants see exactly their project, company analysts see their company, platform administrators retain full reach, and all of it is enforced in one authoritative place.

The rest of this article is how that promise turns into checkboxes and grants you can actually set up.

Custom Roles From a Permission Matrix

A role in PMAP is a named bundle of permissions. You do not pick from a handful of pre-baked archetypes. You assemble the permissions yourself from a matrix of entity types and actions, which means a role can be as narrow or as broad as the work demands.

The matrix has two axes. On one axis sit ten entity types: company, asset, project, finding, report, runbook, rule, integration, scan, and user. On the other axis sit six actions: view, create, update, delete, approve, and export. Any combination of an entity and an action is a permission you can grant, which yields sixty possible pairs in total. A role that holds finding:view and report:export lets its holder read findings and export reports, and does nothing else.

This is the practical heart of least-privilege. Instead of deciding whether someone is broadly trusted, you decide exactly which entities they touch and what they may do to each one. A read-only auditor role might carry finding:view, report:view and report:export. A triage role might add finding:update so the holder can change status, while deliberately withholding finding:delete. An approver role layers on finding:approve for review workflows. None of these descriptions involve writing code, because none of them are anything more than checkboxes against the matrix.

One detail makes this future-proof rather than brittle. The frontend never hardcodes the list of entities and actions. The catalog of every supported entity type and action is served by the GET /admin/permissions endpoint, so the role builder renders its grid from live platform metadata. When the platform’s permission vocabulary grows, the role builder reflects it without a frontend change.

In the Access Management screen the role list appears as a card grid. Each role card shows its name, a coverage bar indicating how many of the sixty pairs it grants, and the most prominent entity-action badges. Editing a role opens a modal with the full interactive grid, including per-row toggles so you can grant or clear all six actions for an entity at once. This article keeps the focus on setting roles up. For a deeper tour of how the matrix is structured and why it is shaped this way, see our forthcoming companion on the 10×6 permission matrix anatomy.

Granting a Role at Global, Company or Project Scope

A role on its own grants nothing. Permissions become real only when you grant a role to a user, and every grant carries a scope. The scope is what answers the question the coarse model could never answer: not what may this person do, but where may they do it.

PMAP offers three scope types, and the difference between them is precise.

A global grant is unrestricted. The user can act on every tenant on the platform, with no company or project boundary applied. Internally this sets the Unrestricted flag on the user’s resolved scope, and it is the right choice for genuinely platform-wide roles and almost nobody else.

A company grant ties the role to one tenant. The user can access the specified company and every project beneath it. If you bring on an analyst who owns an entire client account, a single company-scoped grant covers all of that client’s current and future projects without you re-granting each time a new engagement opens.

A project grant ties the role to one project and stops there. This is the scope you reach for when you staff a consultant onto a single engagement. The user sees that project’s data and nothing adjacent to it.

In the Assign Access flow you select one or more users, pick the role, then pick the scope as a card choice followed by the relevant company or project from a dropdown. An optional expiry date completes the form. The screen submits one grant per selected user, and any partial failure is reported per user, so you always know which assignments landed.

Why Project Scope Does Not Open the Company

The single most important rule in this entire feature is what a project grant deliberately does not do. A project-scoped grant populates the user’s allowed project set and leaves the allowed company set empty. The owning company is excluded on purpose.

This matters because the intuitive shortcut would be a quiet disaster. If a project grant silently added read access to the project’s parent company, then a consultant staffed onto one engagement would inherit visibility into every sibling project at that same company. The platform’s own design note names this exact hazard: adding the owning company would let a project consultant see every sibling project at the same tenant, which is a cross-engagement data leak.

PMAP refuses that shortcut. A project consultant sees their project. They do not see the company. They do not see the next engagement down the hall. The acceptance criterion is unambiguous. Given a user with a project-scoped grant for a project owned by a company, when they call a company-level listing endpoint for that company, no company data is returned, because the company set is empty for project-scoped grants. This is the behavior that lets you confidently put an outside consultant on a single job without auditing every other client relationship first.

Time-Bound Access for Consultant Engagements

Consultant access has a natural shape: it should exist for the duration of an engagement and then stop. Manually remembering to revoke access on the day a contract ends is exactly the kind of task that slips, and slipped revocations are how stale access accumulates.

PMAP lets you attach an expiry to any grant. The grant carries an optional expires_at timestamp, and once you set it the platform does the off-boarding for you. The moment the timestamp lapses, the grant is excluded from the user’s resolved scope. There is no separate cleanup step you have to schedule.

Two mechanisms make the expiry trustworthy rather than approximate. First, the scope cache for that user has its lifetime capped to the soonest expiry across all of their grants. PMAP caches a resolved scope for thirty seconds to keep authorization fast, but for a time-bound grant the cache is never allowed to outlive the grant. If a grant expires in ten seconds, the cache will not hold a stale answer for the full thirty. Access lapses within seconds of the deadline, not at an arbitrary cache boundary.

Second, a background worker keeps the audit trail honest. The expired-grant announcer periodically sweeps for grants whose expiry has passed and which have not yet been announced, emits an access_expired audit event for each, and marks the row so it is never announced twice. The sweep is capped at five hundred rows per run to keep database pressure predictable, and it is idempotent, which makes it safe to run across multiple replicas. If more than five hundred grants expire at once, the next run picks up the remainder.

The practical result is the workflow consultants and administrators both want. You grant a project-scoped role with an expiry that matches the statement of work. The consultant works. On the closing date their access lapses on its own, and the audit log records that it happened.

Immediate Revocation Within Seconds

Expiry handles the planned ending. Revocation handles the unplanned one. When an engagement closes early, a person changes roles, or you simply granted access by mistake, you need that access gone immediately, not whenever a cache happens to refresh.

PMAP revokes a grant by deleting it by ID from the user access list, a single action in the interface. The important part is what happens underneath. Revocation does not wait for the thirty-second cache window to expire. It explicitly invalidates the target user’s scope cache at the moment the grant is removed, so the very next request from that user is evaluated against their new, reduced set of grants.

The platform’s documentation is careful about the role the cache plays here. The thirty-second TTL is a safety net for out-of-band changes, such as someone editing the database directly, and not the boundary that determines correctness. Correctness comes from explicit invalidation. The acceptance criterion states it plainly: when a revocation completes, the target user’s scope cache is invalidated immediately rather than waiting for the TTL, an access_revoked audit event is emitted, and subsequent requests from that user reflect the revoked grant.

So revocation in PMAP is not a request that takes effect eventually. It takes effect on the next request, it is recorded, and you are never left wondering whether a cache somewhere is still serving access you thought you had removed. That is the property you need when off-boarding is the reason you reached for the revoke button.

A related convenience reduces grant sprawl over time. Re-granting the same role at the same scope to the same user does not create a duplicate row. The assignment is upsert-safe, so a repeat grant updates the expiry on the existing grant instead of stacking a second one beside it. Extending a consultant’s engagement is a clean update rather than a growing pile of overlapping grants you later have to reconcile.

The Platform Admin Short-Circuit

Some users need to operate the whole platform, and forcing them to hold a grant for every entity and action would be both tedious and fragile. PMAP handles this with a deliberate short-circuit rather than an enormous role.

A user who holds the platform_admin system role bypasses the permission matrix entirely. Their scope resolves straight to unrestricted, the full matrix is never loaded for them, and they receive reach across all tenants. This keeps the fast path for the most privileged users simple and avoids the maintenance burden of a sixty-pair role that has to be kept exhaustively complete.

Because the short-circuit is powerful, the conditions that trigger it are strict. The platform checks two things before granting wildcard authority: the role must be a system role, and it must carry the canonical platform_admin name. Both conditions are required. A user who creates their own custom role and happens to name it platform_admin gets nothing special, because that custom role is not a system role. The name alone never confers wildcard reach. This closes the obvious impersonation path, where someone tries to mint admin power simply by reusing a privileged name.

System roles are protected at the API layer as well. They are seeded by database migration, and attempts to modify or delete them through the role endpoints are rejected. The acceptance criterion confirms it: an attempt to update a role flagged as a system role is rejected and the role is left unchanged. The platform_admin wildcard cannot be accidentally weakened by an administrator editing a matrix, because the system role it depends on cannot be edited through the API at all.

One more boundary backs all of this. The entire admin route group, which is where roles and grants are managed, is itself gated behind the platform_admin role. A user without it who tries to call any admin endpoint receives an HTTP 403 regardless of whatever custom-role permissions they happen to hold. Who may manage access is itself an access decision, and PMAP enforces it like one.

Effective Permissions Inspector for Audits

The hardest question an auditor asks is also the simplest to phrase: what can this person actually do right now. In a system with custom roles granted at multiple scopes, the honest answer requires combining every grant a user holds, and doing that by hand across several grants is exactly where mistakes creep in.

PMAP answers the question directly with an effective-permissions query. For any user, the platform returns the union of every permission across all of their grants as a single resolved matrix. You do not reconstruct it from the grant table. You read the computed answer.

For a user who holds the platform_admin system role, the query short-circuits to a wildcard matrix, which makes the all-access state unmistakable rather than something you have to infer. For everyone else, the union reflects the real composition of their grants. If a user holds a triage role on one project and a read-only role on a company, the effective view shows the combined permissions that result.

This surfaces in the admin interface for debugging and compliance review, and it changes the character of an access audit. Instead of cross-referencing grants and roles to deduce what someone can do, a compliance officer opens the inspector and reads it. The persona note in the feature documentation captures the intended use exactly: a compliance officer wants to view the union of all permissions a user holds across all of their grants, so they can answer auditor questions without manual cross-referencing. The inspector exists so that the answer to who can do what is something you look up, not something you assemble.

Upsert-Safe Assignment and System Role Protection

A few rules quietly keep the grant table clean and the privileged roles safe, and they are worth setting up your processes around because they prevent the slow accumulation of access debt.

The first is upsert-safe assignment, mentioned earlier in the context of extending engagements. The grant table treats the combination of user, role, scope type and scope target as a unique key. Re-granting that same combination updates the expiry on the existing grant rather than inserting a duplicate. The acceptance criterion is precise: given an existing grant for that combination, when the assignment runs again with a new expiry, no duplicate row is created and the expiry is updated. In day-to-day terms, renewing a consultant is a one-line update, and your grant table never fills with redundant rows that obscure the real picture.

The second is system role immutability, already covered for the platform_admin case but worth restating as a general rule. System roles are migration-managed and cannot be modified or deleted through the API. This is not only about protecting the admin wildcard. It means the foundational roles the platform depends on cannot be reshaped by an ordinary administrative action, deliberate or accidental.

A third rule guards against a subtle history problem. The grant listing filters out grants whose target user has been soft-deleted. This is a defense-in-depth seatbelt for any grant rows that might pre-date a cascade-delete migration, ensuring that a deleted user never appears in the access list with grants that should no longer count. You will rarely notice this rule, which is precisely the point. It keeps the access picture truthful without anyone having to maintain it.

One Authoritative Enforcement Layer

Everything above would be far less trustworthy if access were enforced in many places. Scattered enforcement is how policies drift, where one domain checks a grant correctly and another forgets, and a leak hides in the gap between them.

PMAP avoids that by resolving and enforcing scope in exactly one layer. Every authenticated request passes through the authz.LoadScope middleware. That middleware asks the RBAC layer to resolve the caller’s scope, which produces a single context object carrying the user’s permission matrix, their allowed company set, their allowed project set, and the unrestricted and platform-admin flags. That resolved context is then handed to the downstream repositories, which apply the company and project filters when they read from the database.

The consequence stated in the feature documentation is the one that matters: no domain enforces its own access logic independently. There is one place where a user’s grants become an enforced scope, and every part of the platform consumes that same resolution. When you configure a role and a grant, you are configuring the input to a single, shared enforcement path, not hoping that a dozen independent checks all agree.

This is also why cache invalidation is designed the way it is. Because there is one resolution layer, invalidating one user’s cache on revoke, or purging every user’s cache when a role’s matrix changes, is sufficient to make the change real everywhere. A permission change on a widely held role propagates to all holders on their next request, because they all resolve through the same path. There is no second cache in some other domain quietly serving the old answer.

How PMAP Keeps Least-Privilege Practical

Least-privilege is easy to endorse and hard to live with, because the strictest version of it usually generates so much administrative friction that teams loosen it back to something coarse. PMAP’s RBAC is shaped to make the disciplined version the practical one.

You build roles that say exactly what work requires, no more. You grant them at the scope the engagement actually has, whether that is one project, one company, or the whole platform. You attach an expiry to the access that should be temporary, and it removes itself on schedule with an audit event to prove it. You revoke the access that should stop now, and it stops on the next request rather than thirty seconds later. You read effective permissions instead of reconstructing them. And you do all of it knowing that one enforcement layer turns every grant into the same consistent answer for every domain.

The operational payoff is a platform where putting an outside consultant on a single job is a routine, low-anxiety action, because the scope rules guarantee they cannot wander into adjacent engagements. That confidence is what lets a team running many tenants on one platform staff flexibly without trading away the isolation its clients are paying for.

If you are setting this up for the first time, the step-by-step walkthrough in our guide on configuring RBAC roles and scoped grants takes you from an empty role list to a working time-bound grant. For the wider picture of how scoped access fits into running many tenants on one platform, start with our pillar on multi-tenant vulnerability management. If you are still establishing the vocabulary, our primer on what RBAC is covers the fundamentals, and the permission matrix anatomy piece dissects the sixty pairs in detail. Access setup also pairs closely with the credential layer, so once roles are in place, see how PMAP handles MFA, LDAP and session management for the same operator teams.

PMAP’s RBAC model aligns with the structure described in the NIST RBAC model (INCITS 359) and supports the access-control objectives of the NIST SP 800-53 AC control family and CIS Controls v8 Control 6, Access Control Management. Read the identity and RBAC datasheet and grant exactly the access each engagement needs.

Frequently Asked Questions

What is the difference between a global, company and project grant in PMAP?

A global grant is unrestricted and lets the user act across every tenant on the platform. A company grant ties the user to one company and every project beneath it. A project grant ties the user to a single project only. The key distinction is that a project grant deliberately does not open the owning company, so a project consultant cannot see sibling projects at the same tenant.

How do I build a custom role in PMAP?

You compose a role from a permission matrix of ten entity types and six actions, which gives sixty possible entity-action pairs. You select the exact pairs the role needs, such as finding:view and report:export, name the role, and save it. The role builder renders its grid from a live platform catalog served by the permissions endpoint, so it always reflects the current set of entities and actions without any hardcoded values.

How does time-bound access work for consultants?

You attach an optional expiry timestamp to a grant. When the timestamp passes, the grant is excluded from the user’s resolved scope automatically. The scope cache is capped to the soonest expiry so access lapses within seconds of the deadline rather than at the cache boundary, and a background worker emits an access_expired audit event so the lapse is recorded.

When I revoke a grant, how quickly does it take effect?

Revocation takes effect on the user’s next request, not after a cache delay. PMAP explicitly invalidates the target user’s scope cache the moment the grant is removed, rather than waiting for the thirty-second TTL, and it emits an access_revoked audit event. The TTL exists as a safety net for out-of-band changes, and it is not what determines how fast a revocation lands.

Can someone get full platform access just by naming a role platform_admin?

No. The platform-admin short-circuit requires both that the role is a system role and that it carries the canonical platform_admin name. A custom role someone creates and names platform_admin is not a system role, so it confers no wildcard authority. System roles are seeded by migration and cannot be created, modified or deleted through the API.

How can a compliance officer see everything a specific user can do?

PMAP provides an effective-permissions inspector that returns the union of every permission a user holds across all of their grants as a single resolved matrix, surfaced in the admin interface. For a platform-admin user it returns a wildcard matrix. This lets a reviewer read a user’s real, combined permissions directly instead of cross-referencing individual grants and roles by hand.

Is access enforced consistently across the whole platform?

Yes. Every authenticated request resolves its scope through one middleware layer, which produces a single context with the user’s permissions and allowed companies and projects, and downstream repositories apply that same resolved scope. No individual domain enforces access on its own, which is why a single cache invalidation makes a revoke or a role change effective everywhere on the next request.

author avatar
PMAP Security Team

Newsletter

Get the next writeup in your inbox

One short email when a new case writeup or detection deep dive ships. No marketing drip, no third-party tracking.