Skip to content

fix(auth): route hand-rolled admin-role checks through is_admin_role() and gate the class (#12948) - #12949

Merged
mrveiss merged 5 commits into
Dev_new_guifrom
issue-12786
Jul 29, 2026
Merged

mrveiss merged 5 commits into
Dev_new_guifrom
issue-12786

Conversation

@mrveiss

@mrveiss mrveiss commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Closes #12948. Refs #12704, #12717, #12786 — #12786 stays open for the enum decision.

Thinking Path

#12786 asks for three things. Two of them hinge on an owner decision (whether superadmin/platform_admin become first-class Role members, and the cross-backend permission review that implies). The third — a gate so the class cannot return — is independent, so this delivers that plus the call-site conversion it requires.

The gate found far more than a regex could. I started from a regex sweep that showed 5 offenders. An AST-based check found 17, because the shapes that actually occur are user_data["role"] == "admin", current_user.get("role") == "admin" and reversed operands — none of which a regex keyed on variable names catches. AST also ignores comments and docstrings for free, which a regex flagged as false hits.

Two sites are not "grant superadmin too". Worth reviewing individually:

The first implementation broke the backend. Adding from auth_rbac import is_admin_role to auth_middleware.py closed an import cycle — auth_rbac already imports auth_middleware — and a module-import smoke check dropped from 14/14 on base to 6/14. No unit test would have caught this; the backend simply would not have started.

The fix is not a lazy import. is_admin_role is a pure string predicate with no dependencies, so it belongs beside the Role enum whose gap it covers: autobot_shared/auth/permissions.py imports only enum and typing. auth_rbac re-exports both names (verified to be the identical object) so callers added by PR #12785 keep working.

What Changed

  • autobot_shared/auth/permissions.py: ADMIN_ROLES and is_admin_role() now live here, beside Role, with the cycle rationale recorded.
  • autobot-backend/auth_rbac.py: re-exports both instead of defining them.
  • 17 call sites converted to is_admin_role() across api/ (audit, auth, conversation_files, feature_flags, heartbeat, redis_service, service_messages, ws_security, user_management/{dependencies,users}), auth_middleware.py, llc/api/companies.py, security/enterprise/security_policy_manager.py, and services/ (task_owner, workflow_rbac, mcp_dispatch ×2).
  • services/workflow_rbac.py also carried its own _ADMIN_ROLES = {"admin"} — a fifth forked notion, now removed in favour of the shared one.
  • auth_rbac_admin_guard_test.py (new): AST guard over autobot-backend, plus 11 tests pinning what it must catch (role ==, user.role !=, user_data["role"] ==, .get("role") ==, reversed operands) and what it must not (is_admin_role(...), role in ADMIN_ROLES, non-role names, non-admin literals, docstrings).

Verification

$ python3 -m pytest auth_rbac_admin_guard_test.py security/auth_rbac_test.py -q
39 passed in 17.90s

$ python3 -m flake8 <all 23 changed files>
(clean)

Module-import smoke over the touched modules — the check that caught the cycle:

base    : 14/14
first cut: 6/14      <- import cycle via auth_middleware
final   : 18/18      (18 = the original 14 plus 4 more modules this PR touches)
re-export identical: True | ADMIN_ROLES: ['admin', 'superadmin']
is_admin_role: admin=True  SuperAdmin=True  user=False  None=False

Full suites, branch vs base — identical, so none of the failures are mine:

selection branch base
tests/security/ security/ 18 failed, 216 passed, 1 skipped, 2 xfailed, 25 errors 18 failed, 216 passed, 1 skipped, 2 xfailed, 25 errors
api/user_management/ services/ 45 failed, 2052 passed, 12 skipped, 29 errors 45 failed, 2052 passed, 12 skipped, 29 errors

Those pre-existing failures are environmental — 19 of 69 requirements are not installed locally and there is no Redis. security/enterprise/threat_detection is excluded from both runs for the same reason (ModuleNotFoundError: cachetools, identical on base).

What remains (#12786)

Making superadmin/platform_admin first-class Role members with explicit ROLE_PERMISSIONS entries. That enum drives permissions for both backends, so it needs a cross-backend check that no permission set is silently widened or narrowed — an owner decision, not a drive-by. This PR is the containment that holds the line until then.

Also unaddressed: autobot-slm-backend/services/jwks_verifier.py derives admin from role == "admin" for JWT claims. It is a separate backend that cannot import this helper, and changing token claims has its own blast radius — the guard deliberately scopes to autobot-backend.

Model Used

claude-opus-5

mrveiss added 2 commits July 29, 2026 08:44
…) + guard the class (#12786)

require_role() accepts raw strings, so 17 call sites pass
require_role('admin', 'superadmin') though superadmin is not in the shared Role
enum. Every such guard admits a superadmin while every hand-rolled
role == 'admin' check rejects one, so a superadmin could perform the write but
not the read -- the cause of #12704 and #12717.

17 sites converted to is_admin_role(). An AST guard now fails on any new
role == 'admin' comparison; it found 12 sites a regex sweep had missed
(user_data['role'] == 'admin', .get('role') == ..., reversed operands).

Two are worth calling out as not simple grants:
- security_policy_manager._check_access_policy compares role == 'admin' to
  REQUIRE manager approval, so a superadmin was escaping the approval rule.
- mcp_dispatch blocked superadmin from admin-only tools at one site and served
  them a reduced tool list at another -- the silent-downgrade shape of #12717.

Adding superadmin/platform_admin to the shared Role enum remains open on
#12786: that enum drives ROLE_PERMISSIONS for both backends.
… an import cycle (#12786)

Importing is_admin_role from auth_rbac into auth_middleware closed a cycle --
auth_rbac already imports auth_middleware -- and an import smoke check dropped
from 14/14 to 6/14 modules.

is_admin_role is a pure string predicate, so it belongs beside the Role enum
whose gap it covers, in autobot_shared.auth.permissions (imports only enum and
typing). auth_rbac re-exports both names so existing callers keep working.
@github-actions

Copy link
Copy Markdown
Contributor

✅ SSOT Configuration Compliance: Passing

🎉 No hardcoded values detected that have SSOT config equivalents!

github-actions Bot and others added 3 commits July 29, 2026 06:28
Applied Black, isort, and autoflake to match code-quality checks.
Triggered by workflow auto-fix-formatting.yml.
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.

1 participant