Repository navigation
fix(auth): route hand-rolled admin-role checks through is_admin_role() and gate the class (#12948) - #12949
Merged
Conversation
…) + 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.
Contributor
✅ SSOT Configuration Compliance: Passing🎉 No hardcoded values detected that have SSOT config equivalents! |
Applied Black, isort, and autoflake to match code-quality checks. Triggered by workflow auto-fix-formatting.yml.
This was referenced Jul 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_adminbecome first-classRolemembers, 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:
security_policy_manager._check_access_policycomparesrole == "admin"in order to require manager approval. A superadmin was escaping the approval rule — same class, opposite polarity, and the fix tightens rather than widens.services/mcp_dispatchblocked superadmin from admin-only tools at one site while serving them a reduced tool list at another — the silent-downgrade shape security: voice_bundle_user._is_admin still admin-only — superadmin can't view others' bundles via GET /voice/users/{id}/bundle #12717 reported for voice.The first implementation broke the backend. Adding
from auth_rbac import is_admin_roletoauth_middleware.pyclosed an import cycle —auth_rbacalready importsauth_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_roleis a pure string predicate with no dependencies, so it belongs beside theRoleenum whose gap it covers:autobot_shared/auth/permissions.pyimports onlyenumandtyping.auth_rbacre-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_ROLESandis_admin_role()now live here, besideRole, with the cycle rationale recorded.autobot-backend/auth_rbac.py: re-exports both instead of defining them.is_admin_role()acrossapi/(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, andservices/(task_owner, workflow_rbac, mcp_dispatch ×2).services/workflow_rbac.pyalso 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 overautobot-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
Module-import smoke over the touched modules — the check that caught the cycle:
Full suites, branch vs base — identical, so none of the failures are mine:
tests/security/ security/api/user_management/ services/Those pre-existing failures are environmental — 19 of 69 requirements are not installed locally and there is no Redis.
security/enterprise/threat_detectionis excluded from both runs for the same reason (ModuleNotFoundError: cachetools, identical on base).What remains (#12786)
Making
superadmin/platform_adminfirst-classRolemembers with explicitROLE_PERMISSIONSentries. 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.pyderivesadminfromrole == "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 toautobot-backend.Model Used
claude-opus-5