Skip to content

fix(authz): decouple Ontos admin from settings:ADMIN (#404)#458

Merged
larsgeorge-db merged 2 commits into
mainfrom
fix-404-decouple-admin-from-settings
May 29, 2026
Merged

fix(authz): decouple Ontos admin from settings:ADMIN (#404)#458
larsgeorge-db merged 2 commits into
mainfrom
fix-404-decouple-admin-from-settings

Conversation

@larsgeorge-db
Copy link
Copy Markdown
Collaborator

Summary

Fixes #404. settings:ADMIN was being treated as a proxy for "Ontos admin" — granting a user write access on Settings implicitly turned on admin-only capabilities (most visibly the role switcher in the user menu, plus the alpha-features toggle and MCP token management).

This PR gates admin-only capabilities on membership in an AppRole flagged is_admin=true instead. settings:ADMIN once again only governs administration of the Settings feature.

Changes

Backend

  • AuthorizationManager.is_user_ontos_admin(groups) — canonical predicate. Case-insensitive intersection between the caller's workspace groups and the assigned_groups of any AppRole with is_admin=True. Fails closed if role loading errors.
  • require_ontos_admin FastAPI dependency in common/authorization.py for admin-only endpoints.
  • POST /api/user/role-override derives caller_is_admin from is_user_ontos_admin(user_details.groups) instead of actual_perms['settings'] == ADMIN. Still computed from groups, so an override can never bootstrap admin.
  • GET /api/settings/roles — Ontos admins receive the full catalog (required for the admin role-switcher); non-admins receive only roles whose assigned_groups intersect their own. Stops leaking the full role catalog to anyone authenticated.
  • mcp_tokens_routes.pyrequire_admin is now require_ontos_admin (was PermissionChecker(settings, READ_WRITE)).

Frontend

  • user-info.tsxisAdminActual is now availableRoles.some(r => r.is_admin && r.assigned_groups intersects userInfo.groups). Same variable governs the role switcher impersonation path and the alpha-features toggle.

Tests

  • 8 new unit tests in test_authorization_manager.py covering is_user_ontos_admin (empty groups, case-insensitive match, settings:ADMIN-only roles, no matches, misconfigured admin role with empty assigned_groups, multiple admin roles, settings-manager failures fail closed).
  • New integration suite test_admin_decoupling.py with 8 cases across three classes (roles endpoint scoping, role-override admin gate, MCP tokens gate). Includes the explicit [Bug]: settings:ADMIN implicitly grants admin-level capabilities (e.g. role switcher) #404 regression: a user with settings:ADMIN but no is_admin role membership is denied impersonation and MCP token access.
  • Updated user-info.test.tsx fixtures to carry is_admin; added a regression test that confirms a user with only settings:ADMIN (no is_admin role) sees neither the switcher nor any non-membership roles.

Results: 43/43 across the touched backend test files, 6/6 in user-info.test.tsx.

Out of scope

  • Renaming is_admin or introducing a new app:IMPERSONATE feature (the issue suggested both — we use the existing flag).
  • Persona switcher (only i18n keys exist today).
  • is_user_admin / APP_ADMIN_DEFAULT_GROUPS (separate "workspace admin" concept used for moderation; intentionally untouched).
  • GET /settings/roles/summary (used for dropdowns; revisit if a leak concern surfaces).

Test plan

  • Seed a role Settings Admin with feature_permissions={settings: ADMIN}, is_admin=false, assigned_groups=["settings-admins"]. Log in as a member of settings-admins:
    • Role switcher hidden (unless they belong to 2+ membership-matched roles).
    • Alpha-features toggle hidden.
    • POST /api/user/role-override to a non-member role returns 403.
    • GET /api/settings/roles returns only the Settings Admin role.
    • GET /api/mcp-tokens returns 403.
  • Same user added to a group assigned to the seeded Admin (is_admin=true) role: all admin affordances reappear.
  • Existing Admin-role member (default seed) behaves identically to before.

settings:ADMIN was being used as a proxy for "Ontos admin" in three
places, which let any user with Settings write access impersonate
arbitrary roles via the user-menu role switcher and manage MCP tokens.

Gate admin-only capabilities on membership in an AppRole flagged
is_admin=true instead — the canonical Ontos admin check — and stop
leaking the full role catalog to non-admins.

Changes:
- AuthorizationManager.is_user_ontos_admin(groups): case-insensitive
  intersection with assigned_groups of any is_admin role; fails closed.
- require_ontos_admin FastAPI dependency for admin-only endpoints.
- POST /api/user/role-override now uses is_user_ontos_admin for
  caller_is_admin (computed from groups, never bootstrappable via
  override).
- GET /api/settings/roles returns the full catalog to admins; non-admins
  see only roles whose assigned_groups intersect their own.
- mcp_tokens_routes require_admin switched from PermissionChecker(
  settings, READ_WRITE) to require_ontos_admin.
- Frontend isAdminActual in user-info.tsx computed from is_admin role
  membership instead of actualPermissions['settings']; same variable
  governs the alpha-features toggle.

Tests: 8 new unit tests for is_user_ontos_admin, a new integration suite
covering the three endpoints (roles scoping, role-override admin gate,
MCP tokens gate), and a frontend regression test verifying a user with
settings:ADMIN but no is_admin role no longer sees the switcher or any
non-membership roles.
@larsgeorge-db larsgeorge-db requested a review from a team as a code owner May 27, 2026 21:43
Resolve conflicts where main added complementary helpers next to the
admin-decoupling work:

- src/backend/src/common/authorization.py
  Keep require_ontos_admin (PR) and updated get_user_groups(request=None)
  signature from main side-by-side.
- src/backend/src/controller/authorization_manager.py
  Keep both is_user_ontos_admin (#404) and get_user_effective_role_ids
  (#326).
- src/backend/src/routes/mcp_tokens_routes.py
  Keep require_admin = require_ontos_admin (PR intent — stronger gate
  than the settings-mcp feature permission from main).
- src/backend/src/tests/unit/test_authorization_manager.py
  Merge TestIsUserOntosAdmin (#404 regression coverage) with
  TestGetUserEffectiveRoleIds (#326), giving each class its own fixtures.

No new test failures introduced relative to origin/main.
@larsgeorge-db larsgeorge-db merged commit 9dc6f38 into main May 29, 2026
8 checks passed
larsgeorge-db added a commit that referenced this pull request May 30, 2026
Resolve conflict in src/backend/src/routes/settings_routes.py: keep
the CurrentUserDep gate that main's #458 security-hardening added to
GET /api/settings/ui-customization, while preserving the expanded
docstring describing the new branding fields (app_display_name,
app_short_name, favicon_url). The endpoint is the frontend bootstrap
hook, so it stays open to any authenticated user (no feature
permission) — consistent with /api/settings/llm.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: settings:ADMIN implicitly grants admin-level capabilities (e.g. role switcher)

1 participant