Docs / Admin: Access Model

Access Model

What mcpgate ships at the access-control layer, in NIST terms — and what it deliberately does not, and why.

mcpgate ships NIST RBAC0 with the audit axis of RBAC2: fixed audience tiers, per-action risk classification, an admin-approval gate the model cannot self-grant, and per-user attribution in the audit log. RBAC1 (role hierarchies), the rest of RBAC2 (separation of duties, cardinality), custom roles, per-resource ACL, externalised PDP and JIT elevation are deliberate Nos — different problem shapes, served better at a different layer.

Quick-answer table for procurement reviews

QuestionAnswer
Do you have role-based access control (RBAC)? Yes — at the RBAC0 level, with audit. Fixed roles (admin / internal / external / viewer), per-action classification, admin-approval gate, per-user audit. Details below.
Can we define custom roles? No, deliberately. The four-tier enum is the shipped surface; new roles require code. Custom roles would import policy-DSL complexity that operators consistently said they don’t want.
Role hierarchies (senior inherits junior)? No. The four tiers are flat. No hierarchical resolution.
Separation of Duties (SoD)? No. Belongs in a workflow engine, not an API gateway.
Per-resource ACL (Alice can edit Doc A, not Doc B)? No, by design. The connected service (Drive, Jira, GitLab, Notion) is authoritative for per-resource permissions. mcpgate passes through with the user’s own credentials; the upstream enforces. We do not duplicate or override that layer.
Audit log with user attribution? Yes. Every tool call is recorded with pseudonymised actor, service, action, bytes in/out, result, timestamp. See Compliance.
Externalised PDP (policy in an external service)? No. Internal PDP. Adds zero latency vs. an external policy round-trip. If you need a vendor-grade external policy engine (WorkOS, Oso, Permit.io), run it alongside.
Attribute-based access (ABAC)? Partial. Via data_scope auto-deny rules and policy-hook parameter matching. Not declarative the way Oso or Permit.io are.
Just-in-Time elevation (request → approve → temporary grant)? No. Explicitly dropped in destructive-action governance — the permanent-enable + manual-revoke workflow we observed in production was simpler and sufficient. Add when a real ask appears.

What we have — NIST framing

Users

OAuth-authenticated user identities. Every tool call carries the user that issued it; the audit row carries a pseudonymised hash of that user.

Roles

Fixed enum of four tiers, assignable per user from the admin panel:

  • admin — full control of gateway configuration
  • internal — full access to operator-data and shared services
  • external — restricted; auto-denied on operator-data via data_scope
  • viewer — read-only / further restricted

Admin can assign users to a tier. Admin cannot define a fifth tier. This is RBAC0 deliberately limited. The constraint is the feature: the operator does not maintain a policy IDE.

Permissions

Three orthogonal layers compose the effective permission on any tool call:

  1. Action classification (read / write / high-risk) ships with every action via YAML metadata. Read actions execute. Write actions get per-call confirmation. High-risk actions need explicit admin enable through the Compliance → Destructive Actions surface.
  2. Category policy for the high-risk class — eight rule-based risk categories (Permanent / irreversible, Container destroy, Bulk delete, API passthrough, Scoped, Comment-metadata, Member-access, Recoverable). Admin toggles policy per category; Recoverable never escalates. The mechanism is described in destructive-action governance.
  3. Per-user policy hooks — YAML hooks match on actor_group, action pattern, parameters; deny, enrich or route. Two scopes stack: company-wide (config/tool_hooks.yaml) and per-user. See Hooks reference.

Per operation, not per tool. Some MCP servers expose a few generic tools and put the API operation in an argument. Storyblok's MCP server, for example, runs every delete through one tool. For such a server, the connector tells the gateway which argument names the operation. The category policy then judges each operation, and an admin approval covers that one operation, not every call of the tool. With the default categories, a delete of a whole Storyblok datasource waits for admin approval, and a delete of one story runs. A blocked operation appears in the Destructive Actions queue with its own switch.

Audit (the RBAC2 axis we did keep)

Every tool call writes one audit row: pseudonymised actor, service, action, bytes in/out, result code, timestamp. The row is the single substrate for the Throughput dashboard, the destructive-actions blocked queue, the forensic surface, and the per-user volume page. See Compliance for the layers stacked on top of the audit log.

Permission check (the PEP/PDP shape)

Happens at the gateway gate. The executor reads the effective classification through a strict precedence chain — read-only mode → per-action override → category policy → shipped default — and either runs the call, returns ADMIN_APPROVAL_REQUIRED, or routes through a hook. The PEP (Policy Enforcement Point) is the executor’s check; the PDP (Policy Decision Point) is the category-policy module. Both live in the same process; there is no external policy round-trip on the hot path.

What we deliberately do not have

CapabilityWhy we don’t ship it
Custom roles Would import policy-DSL complexity that operators repeatedly said they don’t want. The four-tier enum covers the common shape; anything beyond it tends to grow into an in-house RBAC IDE we are not staffed to maintain.
Role hierarchies (RBAC1) Flat is enough. Senior-inherits-junior introduces resolution complexity (override semantics, conflict rules) for marginal value at our scale.
Separation of Duties (RBAC2) Belongs in a workflow engine where the state machine lives. Gateways enforce a single point; workflow tools enforce sequences.
Cardinality constraints “Only one user holds role X” constraints belong with custom-roles — same reason as above.
Per-resource ACL The connected service is authoritative for per-resource permissions. mcpgate passes through with the user’s own credentials; the upstream service’s permission model (Drive shares, Jira project roles, GitLab branch protection, Notion page-level perms) enforces. We deliberately do not duplicate or override that layer.
Externalised PDP An external policy round-trip adds ~50–100 ms per tool call. Our internal PDP runs in the same process. If you need vendor-grade external policy (WorkOS, Oso, Permit.io), run that vendor — they are good at it, and mcpgate does not try to be.
JIT elevation Explicitly dropped in the destructive-action governance close: the permanent-enable + manual-revoke workflow we observed in production was simpler and sufficient. Will revisit when a real ask appears.
Full ABAC matrix Partially covered by data_scope auto-deny and policy hook attribute matching. Not declarative the way Oso or Permit.io are. The hook layer is the escape hatch where attribute-based logic actually lives today.

What this shape buys you

The decisions above are coherent, not happenstance. The shape mcpgate ships is a trade:

  • One operator runs governance as a weekly habit, not a daily job. Fixed enum + classified actions + admin gate + audit fits five minutes per week of attention.
  • No external policy-engine dependency. Internal PDP, no latency tax on the hot path, no SLA dependency on a third-party policy service.
  • Single audit substrate. Throughput dashboard, destructive-actions blocked queue, forensic queries and per-user volume all read the same row format.
  • Downstream service permissions stay authoritative. A Drive share decision, a Jira project role, a GitLab branch protection rule — those remain in the source of truth where the operator already manages them. mcpgate does not hide them behind a gateway override.
  • No false RBAC promises. Procurement does not get a tool that ticks an RBAC box and then fails the demo on custom-roles or per-resource ACL. The boundary is named on this page so the conversation stays honest.

What it does not cover — and where to go

If your environment needs capabilities mcpgate deliberately does not ship, the right answer is to pair us with the specialised tool that does:

  • Enterprise per-resource ACL, custom roles with hierarchies, declarative policy DSL → WorkOS, Oso, Permit.io. Run alongside; mcpgate’s hooks can call into them if you need policy externalisation at the gateway layer.
  • Cross-system workflow approvals (multi-step state machines with approvers, escalation, parallel branches) → a workflow engine such as Camunda, n8n, or your existing one.
  • Per-resource permissions on the connected services themselves → the service’s own permission model: Google Drive shares, Jira project roles + permissions schemes, GitLab member roles + branch protection, Notion page-level permissions, etc.

Related

  • Destructive-action governance — the admin-approval gate, the category model, the deliberate-No on audience-tiers-as-matrix that this page formalises
  • Compliance — audit log, PII sanitization, throughput-as-DLP
  • Notifications — how operator alerts are delivered when something fires
  • Hooks reference — policy hooks at company and per-user scope
  • Compare pages — how mcpgate’s access model compares to RBAC-positioned gateway products