Upgrade actions authorized by Owner/Contributor RBAC (no allowlist needed)¶
Motivation¶
Operators saw "You are signed in but not on the upgrade-admin allowlist —
start/rollback/escape-hatch actions are disabled. Ask an operator to add your
oid to UPGRADE_ADMIN_OIDS." even when they were a subscription Owner.
The upgrade-admin gate only recognised the UpgradeAdmin MSAL app role or the
UPGRADE_ADMIN_OIDS env allowlist — there was no path for a normal Azure RBAC
role. Maintaining a separate allowlist/group for what is effectively "can deploy
the control plane" is redundant with the Owner/Contributor a deployer already
holds.
User-facing change¶
- Owner / Contributor on the deployment (subscription or resource group) can
now start / rollback / view the escape hatch with no extra configuration.
Entra group-inherited Owner/Contributor counts (the enumeration uses the
assignedTo()OData filter). - Reader is still rejected — read-only RBAC does not grant upgrade actions.
UPGRADE_ADMIN_OIDSand theUpgradeAdminapp role still work but are now an optional break-glass override for a principal that holds neither an RBAC write role nor the app role.- The SPA warning when blocked now reads "You need an Owner or Contributor role
on the deployment …" instead of pointing at
UPGRADE_ADMIN_OIDS.
API / IaC diff summary¶
api/services/upgrade/auth.py- New
caller_has_platform_write(caller)— reusesapi.services.me_permissions.compute_caller_permissions(the same RBAC enumeration that powers/api/me/permissions) and returns True only whencan_write and not degraded. is_upgrade_adminnow grants via (1) platform write RBAC → (2)UpgradeAdminapp role → (3)UPGRADE_ADMIN_OIDSallowlist. The deployed dev-bypass refusal (Audit P1 #11) is unchanged.- The 403 detail message lists the RBAC option first.
- SECURITY — fails closed.
compute_caller_permissionsopens every capability when enumeration fails (a UX affordance for the SPA). The gate explicitly requiresnot degraded, so a caller whose enumeration failed is NOT auto-promoted — they fall back to the app role / allowlist. api/tests/conftest.py—_env_baselinenow also drops ambientAZURE_SUBSCRIPTION_IDso the RBAC path stays network-free/deterministic in tests unless a test opts in.- SPA:
UpgradePage.tsxwarning copy + module docstring;upgrade.tsroute comments. No new endpoint, no payload change. - No infra change. The shared UAMI already receives subscription-scope
Reader (
assignSubscriptionReader=true, default), which carriesMicrosoft.Authorization/roleAssignments/read— the permission the enumeration needs./api/me/permissionsalready depends on it.
Persona impact (charter §12a Rule 2)¶
This is an additive broadening (Contributor gains upgrade actions); no role is narrowed, so the 2-phase removal rule does not apply. Persona Matrix updated:
| Persona | Before | After |
|---|---|---|
owner_caller (UpgradeAdmin role) |
admin ✓ | admin ✓ (unchanged) |
contributor_caller (write RBAC) |
not admin | admin ✓ (via RBAC) |
reader_caller (read RBAC) |
not admin | not admin (unchanged) |
dev_bypass_caller |
local: needs allowlist | unchanged |
New tests assert the RBAC promotion, the Reader rejection, and that a degraded enumeration does NOT promote (fail-closed).
Validation¶
uv run pytest -q api/tests— 2899 passed, 3 skipped.test_persona_matrix.py:test_contributor_is_upgrade_admin_via_rbac,test_reader_is_not_upgrade_admin,test_degraded_rbac_enumeration_does_not_promote,test_contributor_without_rbac_scope_is_not_admin.test_upgrade_routes.py:test_start_admin_via_platform_rbac_without_allowlist(202),test_start_reader_rbac_is_rejected(403),test_start_enforces_admin_rolestill 403.uv run ruff check(touched) — clean;cd web && npm run build— OK;npx eslint(touched) — clean; docs frontmatter guard — OK.