Skip to content

[BUG]: Team-narrowed admin tokens bypass Layer 1 on admin routes guarded by require_permission #6134

Description

@msureshkumar88

Type of Bug

  • Logic error / unexpected behavior

Observation

services/permission_service.py:127,130 (check_permission()) suppresses admin bypass only for public-only tokens (token_teams == []). With a non-empty token_teams (a team-narrowed admin token), the elif allow_admin_bypass and await self._is_user_admin(user_email): return True branch grants full, unrestricted admin bypass regardless of the token's narrowing.

This contradicts is_unrestricted_platform_admin() (mcpgateway/auth_context.py:788-799), which requires token_teams is None and rejects any narrowed token. The two functions answer the same question — "does this token carry unrestricted admin authority?" — differently.

Where this is reachable

Any route decorated with @require_permission("admin.<something>") rather than the stricter require_global_admin_permission()/require_unrestricted_platform_admin() pair. As of #6132, that's roughly 64 admin routes over records with no team association, spanning:

  • LLM config and admin (llm_config_router.py, llm_admin_router.py)
  • Observability (observability.py)
  • SSO provider management (sso.py)
  • SIEM destinations (siem.py)
  • Log search (log_search.py)
  • Runtime mode (runtime_admin_router.py)
  • Toolops (toolops_router.py)
  • RBAC permission introspection (rbac.py)
  • metrics_maintenance.py (router-level require_admin_auth, same underlying gap)

The full, exact list is enumerated in tests/unit/mcpgateway/test_global_record_scope.py's GLOBAL_ONLY_DEFERRED manifest (added in #6132), which exists specifically to track this population so it can't silently grow.

Why I'm not calling this settled either way

It's not self-evident whether the current behavior is intentional. Two readings:

  1. Bug — Layer 1 narrowing is meant to bind admin bypass everywhere, and check_permission()'s exemption for non-empty token_teams is an oversight that should be closed to match is_unrestricted_platform_admin().
  2. Design choice needing documentation@require_permission(...)-guarded routes were deliberately meant to allow narrowed admin tokens (e.g., because the underlying records are considered lower-risk than roots/compliance/RBAC role management), and this should be documented as an explicit exception rather than treated as a defect.

Maintainers should weigh in on which reading is correct before any fix is scoped.

Reproduction

# services/permission_service.py:121-136
if token_teams is not None and len(token_teams) == 0:
    # Public-only: bypass correctly suppressed
    ...
elif allow_admin_bypass and await self._is_user_admin(user_email):
    # token_teams == ["team-a"] lands here — full bypass, unconditionally
    return True

A caller with a JWT carrying "teams": ["team-a"] and is_admin: true gets unrestricted access to any @require_permission("admin.system_config") route (for example), identical to a caller with "teams": null.

Acceptance Criteria

Scenario: Team-narrowed admin token on a require_permission-guarded global-record route
  Given an admin credential narrowed to one or more teams
  When the caller uses a route guarded by @require_permission("admin.*") that manages
    a record with no team association
  Then maintainers have decided whether this is denied (bug fix) or documented
    as an intentional exception, and the codebase matches that decision

Related

#5982 should stay open until this lands.

Metadata

Metadata

Assignees

No one assigned

    Labels

    apiREST API Related itembugSomething isn't workingrbacRole-based Access ControlsecurityImproves securitytriageIssues / Features awaiting triage

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions