Skip to content

RBAC ConfigurationΒΆ

Role-based access control (RBAC) defines which actions users or teams can perform in ContextForge. This document covers the two-layer security model, token scoping semantics, permission system, and best practices for access control.


OverviewΒΆ

ContextForge implements a two-layer security model:

  1. Token Scoping (Layer 1): Controls what resources a user CAN SEE (data filtering)
  2. RBAC (Layer 2): Controls what actions a user CAN DO (action authorization)

Both layers must pass for an operation to succeed.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                      Two-Layer Security Model                               β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                             β”‚
β”‚   Request β†’ Authentication β†’ Token Scoping β†’ RBAC Check β†’ Operation        β”‚
β”‚                              (Can See?)       (Can Do?)                     β”‚
β”‚                                                                             β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚   β”‚   JWT    β”‚   β”‚  User    β”‚   β”‚ Resource β”‚   β”‚Permissionβ”‚   β”‚ Execute  β”‚ β”‚
β”‚   β”‚  Token   │──▢│ Identity │──▢│  Access  │──▢│  Check   │──▢│ Operationβ”‚ β”‚
β”‚   β”‚          β”‚   β”‚          β”‚   β”‚          β”‚   β”‚          β”‚   β”‚          β”‚ β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚                                                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Authentication MethodsΒΆ

Method Priority Description
JWT Token 1 (Primary) Signature verified, supports teams/scopes claims
Plugin Auth 0 (Before JWT) HTTP_AUTH_RESOLVE_USER hook can provide custom auth
API Token (DB) 2 (Fallback) Legacy database-stored tokens
Proxy Header 3 When MCP_CLIENT_AUTH_ENABLED=false AND TRUST_PROXY_AUTH=true
Anonymous 4 When AUTH_REQUIRED=false (development only)

Core ConceptsΒΆ

SubjectsΒΆ

Users authenticated via:

  • JWT tokens (session or API)
  • SSO providers (OAuth 2.0/OIDC)
  • Basic authentication (development only)

TeamsΒΆ

Logical groups that:

  • Organize users for access boundaries
  • Own resources (tools, prompts, resources)
  • Map from external identity providers (SSO groups)

Built-in RBAC RolesΒΆ

Role Scope Permissions
platform_admin global ["*"] (all permissions)
team_admin team admin.dashboard, gateways.read, gateways.create, gateways.update, gateways.delete, servers.read, servers.create, servers.update, servers.delete, teams.read, teams.update, teams.join, teams.delete, teams.manage_members, tools.read, tools.create, tools.update, tools.delete, tools.execute, resources.read, resources.create, resources.update, resources.delete, prompts.read, prompts.create, prompts.update, prompts.delete, a2a.read, a2a.create, a2a.update, a2a.delete, a2a.invoke, llm.read, llm.invoke, tokens.create, tokens.read, tokens.update, tokens.revoke
developer team admin.dashboard, gateways.read, gateways.create, gateways.update, gateways.delete, servers.read, servers.create, servers.update, servers.delete, teams.read, teams.join, tools.read, tools.create, tools.update, tools.delete, tools.execute, resources.read, resources.create, resources.update, resources.delete, prompts.read, prompts.create, prompts.update, prompts.delete, a2a.read, a2a.create, a2a.update, a2a.delete, a2a.invoke, llm.read, llm.invoke, tokens.create, tokens.read, tokens.update, tokens.revoke
viewer team admin.dashboard, gateways.read, servers.read, teams.read, teams.join, tools.read, resources.read, prompts.read, a2a.read, llm.read, tokens.create, tokens.read, tokens.update, tokens.revoke
platform_viewer global admin.dashboard, gateways.read, servers.read, teams.read, teams.join, tools.read, resources.read, prompts.read, a2a.read, llm.read, tokens.create, tokens.read, tokens.update, tokens.revoke

Default Role Assignment

New users automatically receive up to two roles upon creation:

Admin users (is_admin: true) receive:

  1. platform_admin role with global scope (scope_id = None)
  2. Grants unrestricted access to all platform resources
  3. team_admin role with team scope (scope_id = personal team ID)
  4. Grants full management of their personal team resources
  5. Only assigned if personal team creation succeeds

Non-admin users (is_admin: false) receive:

  1. platform_viewer role with global scope (scope_id = None)
  2. Grants read-only access to all platform resources
  3. team_admin role with team scope (scope_id = personal team ID)
  4. Grants full management of their personal team resources
  5. Only assigned if personal team creation succeeds

This dual-role approach ensures: - Users always have appropriate global visibility (via platform_admin or platform_viewer) - Users can fully manage their personal team resources (via team-scoped team_admin, when available) - Clear separation between team-level and platform-level permissions

The granted_by field tracks which admin created the user for audit purposes.

Existing Users Migration

An Alembic migration (v1a2b3c4d5e6) automatically updates existing users without roles:

Previous behavior (before migration): - Admin users: Only platform_admin with global scope (scope_id = None) - Non-admin users: Only viewer with team scope (scope_id = None)

After migration:

Admin users receive:

  1. team_admin role with team scope
  2. scope_id = user's personal team ID (from email_team_members table)
  3. Enables management of personal team resources
  4. platform_admin role with global scope
  5. scope_id = None
  6. Maintains unrestricted platform access

Non-admin users receive:

  1. team_admin role with team scope
  2. scope_id = user's personal team ID (from email_team_members table)
  3. Enables management of personal team resources
  4. platform_viewer role with global scope
  5. scope_id = None
  6. Provides read-only access to platform resources

Migration behavior: - Only affects users without existing role assignments - Users without a personal team still receive their global role (team_admin is skipped) - The platform admin (configured via PLATFORM_ADMIN_EMAIL) is excluded to preserve bootstrap configuration - Migration is idempotent and safe to run multiple times

ResourcesΒΆ

Protected entities:

  • Servers (MCP gateways and virtual servers)
  • Tools, Prompts, Resources (MCP primitives)
  • System configuration and audit logs

Token Scoping ModelΒΆ

Token scoping controls what resources a token can access based on the teams claim in the JWT payload. The normalize_token_teams() function is the single source of truth for interpreting JWT team claims across all enforcement points.

Token Scoping ContractΒΆ

The teams claim in JWT tokens determines resource visibility. The system follows a secure-first design: when in doubt, access is denied.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        Token Teams Claim Handling                           β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  JWT Claim State          β”‚  is_admin: true       β”‚  is_admin: false        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  No "teams" key           β”‚  PUBLIC-ONLY []       β”‚  PUBLIC-ONLY []         β”‚
β”‚  teams: null              β”‚  ADMIN BYPASS (None)  β”‚  PUBLIC-ONLY []         β”‚
β”‚  teams: []                β”‚  PUBLIC-ONLY []       β”‚  PUBLIC-ONLY []         β”‚
β”‚  teams: ["team-id"]       β”‚  Team + Public        β”‚  Team + Public          β”‚
β”‚  teams: ["t1", "t2"]      β”‚  Both Teams + Public  β”‚  Both Teams + Public    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Admin Bypass Requirements

Admin bypass (unrestricted access) requires BOTH conditions:

  1. teams: null (explicit null, not missing key)
  2. is_admin: true

A missing teams key always results in public-only access, even for admins. An empty teams: [] also results in public-only access, even for admins.

is_admin, teams, and token_use (Mental Model)ΒΆ

These three values are related, but they control different things:

Field Purpose Where It Is Enforced
is_admin User/admin identity flag RBAC permission checks (PermissionService)
teams Visibility/data scope (None, [], or team list) Token scoping and service query filtering
token_use Token interpretation mode (session vs api) Auth pipeline (get_current_user)

Key points:

  1. is_admin does not automatically mean "see everything".
  2. Visibility comes from normalized token_teams:
  3. None = admin bypass scope
  4. [] = public-only scope
  5. ["t1", ...] = team-scoped visibility
  6. token_use decides where teams come from:
  7. session: teams are resolved from DB/cache on each request
  8. api/legacy: teams come from JWT claim via normalize_token_teams()

End-to-End Authorization PipelineΒΆ

  1. Authenticate token and resolve user
  2. Verify JWT/API token.
  3. Resolve token_use.
  4. Normalize/resolve teams
  5. token_use=session β†’ resolve from DB (_resolve_teams_from_db()).
  6. token_use!=session β†’ normalize JWT teams (normalize_token_teams()).
  7. Layer 1: Token scoping
  8. Filter accessible resources by token_teams.
  9. Public-only tokens cannot access team/private resources.
  10. Layer 2: RBAC permission check
  11. Validate action permission (tools.read, admin.system_config, etc.).
  12. Public-only guard: admin.* permissions are denied when token_teams=[].

Why Public-Only Admin Tokens Can Still Be Useful

A token can represent an admin identity (is_admin=true) while still being visibility-restricted (teams=[]). This allows controlled automation tokens with reduced blast radius.

Return Value SemanticsΒΆ

Return Value Meaning Query Behavior
None Admin bypass Skip ALL visibility filtering (requires user_email=None in the service layer)
[] (empty list) Public-only Filter to visibility='public' ONLY β€” owner and team access are both suppressed
["t1", "t2"] Team-scoped Filter to team resources + public + user's own private resources

Owner Access is Scoped to Private Visibility

Owner-based access (owner_email matching) grants visibility only for resources with visibility='private'. Resource owners cannot use ownership to bypass team scoping β€” a team-visibility resource is only visible if the user's token_teams includes that team.

Security Design PrinciplesΒΆ

  1. Secure-First Defaults

  2. Missing teams key always returns [] (public-only access)

  3. This prevents accidental exposure when tokens are misconfigured

  4. Explicit Admin Bypass

  5. Admin bypass requires explicit teams: null AND is_admin: true

  6. Empty teams [] disables bypass even for admins

  7. Scoped Automation Tokens

  8. Tokens with teams: [] are intentionally restricted to public resources

  9. Use case: CI/CD pipelines, monitoring systems, public API clients

Token Scoping FlowΒΆ

                                 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                 β”‚   JWT Token      β”‚
                                 β”‚   Received       β”‚
                                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                          β”‚
                                          β–Ό
                              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                              β”‚  Extract "teams"      β”‚
                              β”‚  claim from JWT       β”‚
                              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                          β”‚
                          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                          β”‚                               β”‚
                          β–Ό                               β–Ό
               β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
               β”‚ "teams" key EXISTS  β”‚       β”‚ "teams" key MISSING β”‚
               β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                          β”‚                             β”‚
                          β–Ό                             β–Ό
               β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
               β”‚ Check teams value   β”‚       β”‚ Return []           β”‚
               β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β”‚ PUBLIC-ONLY         β”‚
                          β”‚                  β”‚ (secure default)    β”‚
          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚               β”‚               β”‚
          β–Ό               β–Ό               β–Ό
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ teams: null   β”‚ β”‚ teams: [] β”‚ β”‚ teams: [...]  β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚               β”‚               β”‚
          β–Ό               β”‚               β–Ό
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”‚       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ Check is_adminβ”‚       β”‚       β”‚ Return [...]  β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜       β”‚       β”‚ TEAM-SCOPED   β”‚
          β”‚               β”‚       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    β”Œβ”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”         β”‚
    β”‚           β”‚         β”‚
    β–Ό           β–Ό         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Admin  β”‚ β”‚Non-Adm β”‚ β”‚ Empty  β”‚
β”‚ true   β”‚ β”‚ false  β”‚ β”‚ list   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Return β”‚ β”‚ Return β”‚ β”‚ Return β”‚
β”‚ None   β”‚ β”‚ []     β”‚ β”‚ []     β”‚
β”‚ BYPASS β”‚ β”‚ PUBLIC β”‚ β”‚ PUBLIC β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key Insight

The difference between teams: null and missing teams key is critical:

  • Missing key: Always [] (public-only) - secure default
  • Explicit null: Admin bypass when is_admin: true, otherwise []

Visibility LevelsΒΆ

Resources in ContextForge have three visibility levels:

Visibility Description Who Can See
public Accessible to all authenticated users Everyone with valid token
team Accessible to team members only Team members + admins (with bypass)
private Accessible to owner only Resource owner + admins (with bypass)

Access Matrix by Token TypeΒΆ

Token Type Public Resources Team Resources Private Resources
Admin Bypass (teams=null, is_admin=true) βœ… βœ… (all teams) βœ… (all)
Team-Scoped (teams=["t1"]) βœ… βœ… (own team) βœ… (own only)
Public-Only (teams=[]) βœ… ❌ ❌

Public-Only Token Limitations

Public-only tokens (teams=[]) cannot access private resources, even if the resource is owned by the token's user.

This is intentional security behavior - public-only tokens are designed for limited-scope access to public resources only. To access private resources, users must use a team-scoped token that includes their personal team.

# Public-only token behavior:
# βœ… Can access: visibility='public' resources
# ❌ Cannot access: visibility='team' resources (any team)
# ❌ Cannot access: visibility='private' resources (even if owned by user)

Enforcement PointsΒΆ

Token scoping is enforced consistently across all access paths:

Location Token Scoping RBAC Description
Token Scoping Middleware βœ… N/A Request-level data filtering
REST API Endpoints βœ… βœ… @require_permission decorators
RPC Handler (/rpc) βœ… Varies Method-specific permission checks
Admin UI βœ… βœ… Permission-based UI rendering
Service Layer βœ… N/A Database query filtering
WebSocket βœ… βœ… Forwards auth to /rpc
MCP Transport βœ… N/A Streamable HTTP protocol filtering

Method-Level RBAC ExamplesΒΆ

The /rpc endpoint applies method-level authorization for sensitive operations. These checks are aligned with equivalent REST endpoints.

Method / Endpoint Required Permission Notes
JSON-RPC logging/setLevel (POST /rpc) admin.system_config Same permission as POST /logging/setLevel
Utility SSE (GET /sse) tools.execute Canonical tool execution permission
Utility message relay (POST /message) tools.execute Canonical tool execution permission

Token Types and Use CasesΒΆ

Session Tokens (UI Login)ΒΆ

Generated when users log in via the Admin UI:

{
  "sub": "admin@example.com",
  "user": {
    "email": "admin@example.com",
    "is_admin": true
  },
  "token_use": "session",
  "iss": "mcpgateway",
  "aud": "mcpgateway-api",
  "exp": 1234567890
}

Behavior: Session tokens resolve teams server-side per request. For admin users, resolved teams become None (admin bypass). For non-admin users, resolved teams become their DB membership list (or [] if none).

API Tokens (Programmatic Access)ΒΆ

Generated via the Admin UI or API for automation:

{
  "sub": "service-account@example.com",
  "is_admin": false,
  "teams": ["team-uuid-1", "team-uuid-2"],
  "iss": "mcpgateway",
  "aud": "mcpgateway-api",
  "exp": 1234567890
}

Behavior: Access restricted to public resources plus resources owned by specified teams.

Scoped Automation TokensΒΆ

For CI/CD, monitoring, or public API access:

{
  "sub": "ci-pipeline@example.com",
  "is_admin": true,
  "teams": [],  // Explicitly empty = public-only
  "iss": "mcpgateway",
  "aud": "mcpgateway-api",
  "exp": 1234567890
}

Behavior: Even admin tokens with teams: [] are restricted to public resources only. This enables creating limited-scope tokens for automation that shouldn't access team-internal resources.


Generating Scoped TokensΒΆ

Using the CLI ToolΒΆ

# Unrestricted admin-bypass API token (explicit teams=null + is_admin=true)
python3 -m mcpgateway.utils.create_jwt_token \
  --data '{"sub":"admin@example.com","is_admin":true,"teams":null,"token_use":"api"}' \
  --exp 60 \
  --secret $JWT_SECRET_KEY

# Team-scoped token
python3 -m mcpgateway.utils.create_jwt_token \
  --data '{"sub":"user@example.com","is_admin":false,"teams":["team-uuid-1"],"token_use":"api"}' \
  --exp 60 \
  --secret $JWT_SECRET_KEY

# Public-only scoped token (for automation)
python3 -m mcpgateway.utils.create_jwt_token \
  --data '{"sub":"ci@example.com","is_admin":false,"teams":[],"token_use":"api"}' \
  --exp 60 \
  --secret $JWT_SECRET_KEY

Using the Admin UIΒΆ

  1. Navigate to Admin UI β†’ Tokens
  2. Click Create Token
  3. Select team scope:

  4. No team selected: Public resources only (secure default)

  5. Specific team(s): Team + public resources

  6. Configure additional restrictions (IP, permissions, expiry)

Token Scope Warning

Tokens created without selecting a team will have access to public resources only. This is the secure default to prevent accidental exposure of team resources.


Permission SystemΒΆ

Permission CategoriesΒΆ

Permissions are defined in the Permissions class and control what actions users can perform:

Category Permissions
Users users.create, users.read, users.update, users.delete, users.invite
Teams teams.create, teams.read, teams.update, teams.delete, teams.join, teams.manage_members
Tools tools.create, tools.read, tools.update, tools.delete, tools.execute
Resources resources.create, resources.read, resources.update, resources.delete, resources.share
Gateways gateways.create, gateways.read, gateways.update, gateways.delete
Prompts prompts.create, prompts.read, prompts.update, prompts.delete, prompts.execute
Servers servers.create, servers.read, servers.update, servers.delete, servers.manage
Tokens tokens.create, tokens.read, tokens.update, tokens.revoke
Admin admin.system_config, admin.user_management, admin.security_audit, admin.overview, admin.dashboard, admin.events, admin.grpc, admin.plugins
A2A a2a.create, a2a.read, a2a.update, a2a.delete, a2a.invoke
Tags tags.read, tags.create, tags.update, tags.delete
Wildcard * (all permissions)

Permission Checking FlowΒΆ

@require_permission("resource.action")
    β”‚
    β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Extract user_context        β”‚ ← From request/kwargs
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    β”‚
    β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Plugin Permission Hook      β”‚ ← HTTP_AUTH_CHECK_PERMISSION can override
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    β”‚ (no plugin decision)
    β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Admin Bypass Check          β”‚ ← If allow_admin_bypass=True AND user.is_admin
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    β”‚ (not admin or bypass disabled)
    β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Role Collection             β”‚ ← Get all active roles for user
β”‚ - Global scope roles        β”‚
β”‚ - Personal scope roles      β”‚
β”‚ - Team scope roles          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    β”‚
    β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Permission Aggregation      β”‚ ← Collect permissions from roles
β”‚ - Include inherited perms   β”‚   (role inheritance supported)
β”‚ - Check for wildcard (*)    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    β”‚
    β–Ό
  GRANT or DENY

Explicit Team/Token DefaultsΒΆ

Team and token management behavior is now controlled through explicit role permissions, not implicit fallback checks.

Role Explicit Baseline Grants
team_admin teams.read, tokens.create, tokens.read, tokens.update, tokens.revoke
developer teams.read, tokens.create, tokens.read, tokens.update, tokens.revoke
viewer teams.read, tokens.create, tokens.read, tokens.update, tokens.revoke
platform_viewer teams.read, tokens.create, tokens.read, tokens.update, tokens.revoke

Users without role assignments do not receive implicit team/token permissions.

Admin API RBACΒΆ

The Admin API enforces strict RBAC where even users with is_admin: true must have explicit permissions granted. This enables delegated administration - granting specific admin capabilities without full superuser access.

Key behaviors:

Aspect Behavior
Admin bypass allow_admin_bypass=False on all admin routes
is_admin flag Does NOT bypass permission checks
UI entry Requires any admin.* permission via has_admin_permission()
Route protection All 177 admin routes use @require_permission decorators

Example: Delegated Server Management

{
  "role": "server-manager",
  "permissions": [
    "servers.read",
    "servers.create",
    "servers.update",
    "servers.delete"
  ]
}

A user with this role can:

  • βœ… Access /admin/servers/* endpoints
  • βœ… View the Admin UI (has servers.* which satisfies has_admin_permission())
  • ❌ Access /admin/tools/* endpoints (no tools.* permissions)
  • ❌ Access /admin/gateways/* endpoints (no gateways.* permissions)

Platform Admin Role

The built-in platform_admin role has ["*"] (wildcard) permissions, which grants access to all operations. For delegated administration, create custom roles with specific permission sets.


Configuration SafetyΒΆ

Development vs Production SettingsΒΆ

The following configuration combinations require careful consideration:

Setting Value Impact Recommended Use
AUTH_REQUIRED false All requests granted admin access Development only
TRUST_PROXY_AUTH true + MCP_CLIENT_AUTH_ENABLED=false Trust X-Forwarded-User header without verification Behind trusted reverse proxy only

Proxy Authentication ModeΒΆ

When MCP_CLIENT_AUTH_ENABLED=false and TRUST_PROXY_AUTH=true:

  • The gateway trusts the X-Forwarded-User header from upstream proxy
  • No JWT validation or database verification is performed
  • Only use when deployed behind a trusted reverse proxy that handles authentication

Security Warning

Proxy authentication mode should only be used in trusted network environments where the reverse proxy is the only entry point to the gateway. Exposing the gateway directly to untrusted networks with this configuration allows header injection attacks.

Anonymous Mode (AUTH_REQUIRED=false)ΒΆ

When AUTH_REQUIRED=false:

  • All unauthenticated requests receive platform-admin context
  • Never use in production - all users have full admin access
  • Intended only for local development and testing

Production Warning

Setting AUTH_REQUIRED=false in production grants administrative access to all requests. This completely bypasses authentication and authorization.


Best PracticesΒΆ

Token LifecycleΒΆ

  1. Use short expiration times for interactive sessions (hours)
  2. Use longer expiration for service accounts with IP restrictions
  3. Rotate tokens regularly (recommended: 90 days for long-lived tokens)
  4. Revoke tokens immediately when access should be removed

Team OrganizationΒΆ

  1. Create purpose-specific teams:

  2. platform-admins - Full administrative access

  3. developers - Development and testing resources
  4. ci-automation - CI/CD pipeline access
  5. monitoring - Read-only observability access

  6. Map SSO groups to teams for automatic membership management

  7. Use personal teams for individual resource ownership

Scoping StrategyΒΆ

Use Case Recommended Token Scope
Admin UI access Session token (teams: null + is_admin: true)
CI/CD pipeline teams: [] (public-only)
Service integration Specific team(s)
Developer access Personal team + project teams
Monitoring/alerting teams: [] with read permissions

TroubleshootingΒΆ

Token Not Seeing Expected ResourcesΒΆ

  1. Check token claims: Decode the JWT to verify teams claim

    # Decode JWT payload (middle section)
    echo "$TOKEN" | cut -d. -f2 | base64 -d | jq .
    

  2. Verify resource visibility: Check the resource's visibility and team_id

    curl -H "Authorization: Bearer $ADMIN_TOKEN" /tools/{id} | jq '{visibility, teamId}'
    

  3. Check user admin status: Non-admin users without teams get public-only access

Admin Token Being RestrictedΒΆ

If an admin token is unexpectedly restricted:

  1. Check for explicit teams claim: teams: [] restricts even admins
  2. Verify is_admin flag: Must be true in JWT or database user
  3. Check middleware logs: Look for "token_teams" in debug output

Inconsistent Results Between EndpointsΒΆ

If REST and RPC endpoints return different results:

  1. Check for caching: REST list endpoints may have cached data
  2. Wait for cache TTL: Default is 60 seconds for registry cache
  3. Use direct GET: /tools/{id} bypasses list cache

Bootstrap Custom RolesΒΆ

ContextForge allows you to define custom roles that are automatically created during database bootstrap. This is useful for organizations that need to pre-configure roles before deployment.

ConfigurationΒΆ

Enable custom role bootstrapping with these environment variables:

Variable Default Description
MCPGATEWAY_BOOTSTRAP_ROLES_IN_DB_ENABLED false Enable loading additional roles from file
MCPGATEWAY_BOOTSTRAP_ROLES_IN_DB_FILE additional_roles_in_db.json Path to the JSON file containing role definitions

Role Definition FormatΒΆ

Create a JSON file containing an array of role definitions:

[
  {
    "name": "data_analyst",
    "description": "Read-only access for data analysis",
    "scope": "team",
    "permissions": ["tools.read", "resources.read", "prompts.read"],
    "is_system_role": true
  },
  {
    "name": "auditor",
    "description": "Compliance audit access",
    "scope": "global",
    "permissions": ["tools.read", "resources.read", "prompts.read", "servers.read", "gateways.read"],
    "is_system_role": true
  }
]

Required fields:

  • name - Unique role name
  • scope - Either team (team-level access) or global (system-wide access)
  • permissions - Array of permission strings (e.g., tools.read, resources.create)

Optional fields:

  • description - Human-readable description
  • is_system_role - Set to true to prevent users from modifying/deleting the role

Available PermissionsΒΆ

Resource Permissions
Tools tools.create, tools.read, tools.update, tools.delete, tools.execute
Resources resources.create, resources.read, resources.update, resources.delete
Prompts prompts.create, prompts.read, prompts.update, prompts.delete
Servers servers.create, servers.read, servers.update, servers.delete
Gateways gateways.create, gateways.read, gateways.update, gateways.delete
Teams teams.create, teams.read, teams.update, teams.delete, teams.join

Docker Compose ExampleΒΆ

services:
  gateway:
    environment:
      - MCPGATEWAY_BOOTSTRAP_ROLES_IN_DB_ENABLED=true
      - MCPGATEWAY_BOOTSTRAP_ROLES_IN_DB_FILE=/app/custom_roles.json
    volumes:
      - ./custom_roles.json:/app/custom_roles.json:ro

Kubernetes/Helm ExampleΒΆ

# values.yaml
mcpContextForge:
  env:
    MCPGATEWAY_BOOTSTRAP_ROLES_IN_DB_ENABLED: "true"
    MCPGATEWAY_BOOTSTRAP_ROLES_IN_DB_FILE: "/config/custom_roles.json"

  # Mount ConfigMap with role definitions
  extraVolumes:
    - name: custom-roles
      configMap:
        name: mcp-gateway-roles
  extraVolumeMounts:
    - name: custom-roles
      mountPath: /config

Error HandlingΒΆ

  • File not found: Bootstrap continues with default roles only; warning logged
  • Invalid JSON: Bootstrap continues with default roles only; error logged
  • Malformed entries: Invalid role entries are skipped with warnings; valid entries are processed

Idempotent Bootstrap

Bootstrap is idempotent - running it multiple times won't duplicate roles. Existing roles are detected and skipped.