Type of Bug
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:
- 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().
- 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.
Type of Bug
Observation
services/permission_service.py:127,130(check_permission()) suppresses admin bypass only for public-only tokens (token_teams == []). With a non-emptytoken_teams(a team-narrowed admin token), theelif allow_admin_bypass and await self._is_user_admin(user_email): return Truebranch grants full, unrestricted admin bypass regardless of the token's narrowing.This contradicts
is_unrestricted_platform_admin()(mcpgateway/auth_context.py:788-799), which requirestoken_teams is Noneand 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 stricterrequire_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_router.py,llm_admin_router.py)observability.py)sso.py)siem.py)log_search.py)runtime_admin_router.py)toolops_router.py)rbac.py)metrics_maintenance.py(router-levelrequire_admin_auth, same underlying gap)The full, exact list is enumerated in
tests/unit/mcpgateway/test_global_record_scope.py'sGLOBAL_ONLY_DEFERREDmanifest (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:
check_permission()'s exemption for non-emptytoken_teamsis an oversight that should be closed to matchis_unrestricted_platform_admin().@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
A caller with a JWT carrying
"teams": ["team-a"]andis_admin: truegets unrestricted access to any@require_permission("admin.system_config")route (for example), identical to a caller with"teams": null.Acceptance Criteria
Related
@require_permissionrule that fix: standardize scope checks for admin endpoints managing global records #6132 deliberately deferred).tests/unit/mcpgateway/test_global_record_scope.py) already inventories every affected route inGLOBAL_ONLY_DEFERRED, so resolving this issue is largely a matter of migrating those routes torequire_global_admin_permission()once the intended-behavior question above is settled.#5982 should stay open until this lands.