Console Admin RBAC Architecture

Audience: engineers and operators working with the Authority-backed Console admin surface, and anyone reasoning about Stella Ops scopes and role bundles. Source of truth: the canonical scope catalog is StellaOpsScopes.cs; the seeded role bundles are ConsoleAdminEndpointExtensions.GetDefaultRoles(). Where this doc and the code disagree, the code wins — several rows below are explicitly marked proposed vs seeded.

1. Purpose

2. Scope

Non-goals:

3. Core Architecture

3.1 Authority admin tiers

NOTE (doc-vs-code): an earlier draft described a separate /admin tier gated by mTLS plus an authority.admin scope. Neither exists in source: there is no authority.admin scope constant in StellaOpsScopes.cs, and the Console admin endpoints are not bound to an mTLS-only route group. Authority does support mutual-TLS sender-constrained tokens generally (AuthorityClientCertificateValidator), but the Console admin tiers above authorize on bearer scopes, not client certificates. The /api/admin alias is the only second tier, and it uses the same UI scopes — not mTLS.

3.2 Entities and ownership

Authority remains the source of truth for:

3.3 Fresh-auth

High-risk mutating operations are wired to a fresh-auth gate via RequireFreshAuth() in code:

Authority enforces these operations against the authenticated principal’s auth_time claim. The claim must be a Unix timestamp no more than five minutes old; timestamps more than one minute in the future are also rejected. Missing, invalid, stale, or excessively future authentication returns HTTP 401 with code fresh_auth_required. Read-only admin routes do not apply this filter, so an older session may inspect state but must reauthenticate before changing it. The general Console admin and process-global identity-provider admin surfaces use the same five-minute/one-minute contract.

4. Scope and Role Taxonomy

4.1 Console admin scopes

Admin scopes (Authority-managed, all defined in StellaOpsScopes.cs):

The console Identity Providers route requires ui.admin together with authority:idp.read. Its create, update, delete, enable/disable, test, and apply controls require authority:idp.write and use the same fresh-authentication contract as the Authority mutation endpoints. Legacy platform.idp.read and platform.idp.admin claims do not authorize this Authority-owned surface.

4.2 Scanner scope and role bundles

Scanner scopes and role bundles (all five scopes exist in StellaOpsScopes.cs; the four scanner roles are seeded built-ins in GetDefaultRoles()):

Doc-vs-code (catalog table below): §4.3 augments these scanner bundles with findings:read and aoc:verify. The seeded GetDefaultRoles() catalog does not include those extra scopes on the scanner roles — the built-ins carry only the scanner:* scopes listed here. The §4.3 entries with findings:read/aoc:verify are proposed, not the seeded reality.

Compatibility:

4.3 Module role bundle catalog

Role bundles are grouped by module and map to existing Authority scopes unless noted.

Seeded vs. proposed (doc-vs-code, important). The only role bundles actually seeded as built-ins by Authority today are the ten returned by ConsoleAdminEndpointExtensions.GetDefaultRoles(): role/console-viewer, role/console-admin, role/executive-assurance-viewer, role/release-operations-steward, role/security-risk-manager, role/service-release-contributor, role/scanner-viewer, role/scanner-operator, role/scanner-signed-sbom-writer, role/scanner-admin. Every other row in the module table below (Policy, Concelier, Excititor, Notify, Scheduler, Orchestrator, Graph, Vuln Explorer, Export Center, Advisory AI, Signals, Evidence Locker, Observability, Issuer Directory, Task Packs, Airgap, Exceptions, Attestor, Signer, SBOM, Release, Zastava, and the operator/auditor Operator Verify rows) is a proposed/reference bundle: the underlying scopes generally exist in StellaOpsScopes.cs, but the role itself is not seeded as a built-in. Operators can recreate any of these as custom roles via POST /console/admin/roles (which accepts any catalog scope and auto-creates the permission rows). The role IDs marked (new) in the Scheduler rows refer to a no-longer-accurate “scope is new” annotation — see the Missing-scopes note in this section. Where a proposed row’s scopes differ from a seeded built-in (notably the Scanner rows, which the table augments with findings:read/aoc:verify that the seeded scanner roles do not carry), treat the seeded GetDefaultRoles() definition as authoritative.

Service-contributor deployment is a scoped release-control case, not an admin case. Use role/service-release-contributor for users who can update or request updates for their own microservice images in allowed environments. That role must be paired with ABAC constraints such as serviceIds, repositoryIds, imageNamespaces, environmentIds, and maxEnvironmentType; do not grant broad release:publish, release:bypass, ui.admin, orch:operate, or policy:approve just to enable self-service updates. Authority also narrows interactive console authorization-code grants for role/service-release-contributor and legacy role/release-developer principals to the self-service allow-list, so a broad UI client request does not mint admin/publisher/approval scopes for a contributor.

Console administrators manage those attributes from Identity & Access > Users. Selecting role/service-release-contributor opens fields for service IDs, repository IDs, image namespaces, environment IDs, maximum environment type, and directUpdateAllowed. The Authority admin API persists these values under user metadata attributes, returns them in user summaries, and the standard/LDAP/AD claim enrichers can project them into issued tokens. The UI blocks save until the administrator explicitly confirms any combination of service-release contribution with broad admin, operator, bypass, publisher, or approver roles. Built-in roles returned by GetDefaultRoles() are valid user-update assignments even when an installation has no matching tenant-local role-table row; their authoritative scope bundles remain the compiled catalog. Tenant-created roles still require a persisted role row before assignment.

The customer-facing default bundles are professional functions rather than job titles:

Customer functionBuilt-in roleScope boundary
Executive assurancerole/executive-assurance-viewerRead release, policy audit, findings, evidence, graph, exports, authority audit, tenant timeline, platform context, and (BAZ-3) attestor evidence-transparency / proof-chain via attest:read.
Release operationsrole/release-operations-stewardOperate release orchestration and scans, and read the tenant timeline, without release bypass or platform admin.
Security riskrole/security-risk-managerRead tenant registry image inventory plus the release and integration context used by visible dashboard, Promotions, image, custody, and audit surfaces; investigate findings and vulnerabilities; run policy simulations; read advisory/VEX context, evidence, and the tenant timeline; and (BAZ-3) read the remediation console surface via remediation:read. release:read, registry:read, and integration:read do not grant release mutation, registry administration, or integration write/operate actions, and remediation:read does not grant remediation submit/manage (admin-only).
Service deliveryrole/service-release-contributorRequest or perform ABAC-limited service image updates and read the tenant timeline; no publish, bypass, approval, admin, or broad orchestration operate scopes.
ModuleRole bundleScopes
Consolerole/console-viewerui.read
Consolerole/console-adminui.read, ui.admin, authority:tenants.read, authority:tenants.write, authority:users.read, authority:users.write, authority:roles.read, authority:roles.write, authority:clients.read, authority:clients.write, authority:tokens.read, authority:tokens.revoke, authority.audit.read, authority:branding.read, authority:branding.write, authority:idp.read, authority:idp.write
Consolerole/console-superadmin (proposed; not seeded)ui.read, ui.admin, authority:tenants.*, authority:users.*, authority:roles.*, authority:clients.*, authority:tokens.*, authority.audit.read, authority:branding.* — NB there is no distinct console-superadmin built-in; the seeded role/console-admin already carries the full set. Audit scope is dot-form authority.audit.read.
Operator Verifyoperatoroperator.verify.read, operator.verify.admin
Operator Verifyauditoroperator.verify.read
Scannerrole/scanner-viewerscanner:read, findings:read, aoc:verify
Scannerrole/scanner-operatorscanner:read, scanner:scan, scanner:export, findings:read, aoc:verify
Scannerrole/scanner-signed-sbom-writerscanner.signed-sbom-material.write
Scannerrole/scanner-adminscanner:read, scanner:scan, scanner:export, scanner:write, findings:read, aoc:verify
Policyrole/policy-authorpolicy:read, policy:author, policy:simulate, findings:read
Policyrole/policy-reviewerpolicy:read, policy:review, policy:simulate, findings:read
Policyrole/policy-approverpolicy:read, policy:review, policy:approve, policy:simulate, findings:read
Policyrole/policy-operatorpolicy:read, policy:operate, policy:run, policy:activate, policy:publish, policy:promote, policy:simulate, findings:read
Policyrole/policy-auditorpolicy:read, policy:audit, findings:read
Concelierrole/concelier-readeradvisory:read, aoc:verify
Concelierrole/concelier-ingestadvisory:ingest, advisory:read, aoc:verify
Concelierrole/concelier-operatorconcelier.jobs.trigger, advisory:read, aoc:verify
Concelierrole/concelier-adminconcelier.jobs.trigger, concelier.merge, advisory:read, aoc:verify
Excititorrole/excititor-readervex:read, aoc:verify
Excititorrole/excititor-ingestvex:ingest, vex:read
Notifyrole/notify-viewernotify.viewer
Notifyrole/notify-operatornotify.viewer, notify.operator
Notifyrole/notify-adminnotify.viewer, notify.operator, notify.admin
Schedulerrole/scheduler-viewerscheduler:read
Schedulerrole/scheduler-operatorscheduler:read, scheduler:operate
Schedulerrole/scheduler-adminscheduler:read, scheduler:operate, scheduler:admin
Orchestratorrole/orch-viewerorch:read, findings:read
Orchestratorrole/orch-operatororch:read, orch:operate, findings:read
Orchestratorrole/orch-adminorch:read, orch:operate, orch:quota, orch:backfill, findings:read
Graphrole/graph-viewergraph:read, graph:export
Graphrole/graph-operatorgraph:read, graph:export, graph:simulate
Graphrole/graph-admingraph:read, graph:export, graph:simulate, graph:write, graph:admin
Vuln Explorerrole/vuln-viewervuln:view, findings:read
Vuln Explorerrole/vuln-investigatorvuln:view, vuln:investigate, findings:read
Vuln Explorerrole/vuln-operatorvuln:view, vuln:investigate, vuln:operate, findings:read
Vuln Explorerrole/vuln-auditorvuln:view, vuln:audit, findings:read
Export Centerrole/export-viewerexport.viewer
Export Centerrole/export-operatorexport.viewer, export.operator
Export Centerrole/export-adminexport.viewer, export.operator, export.admin
Advisory AIrole/advisory-ai-vieweradvisory-ai:view, aoc:verify
Advisory AIrole/advisory-ai-operatoradvisory-ai:view, advisory-ai:operate, aoc:verify
Advisory AIrole/advisory-ai-adminadvisory-ai:view, advisory-ai:operate, advisory-ai:admin, aoc:verify
Signalsrole/signals-viewersignals:read, aoc:verify
Signalsrole/signals-uploadersignals:read, signals:write, aoc:verify
Signalsrole/signals-adminsignals:read, signals:write, signals:admin, aoc:verify
Evidence Lockerrole/evidence-readerevidence:read
Evidence Lockerrole/evidence-creatorevidence:read, evidence:create
Evidence Lockerrole/evidence-legalevidence:read, evidence:hold
Observabilityrole/observability-viewerobs:read, timeline:read, attest:read
Observabilityrole/observability-investigatorobs:read, timeline:read, timeline:write, evidence:read, evidence:create, attest:read
Observabilityrole/observability-incident-commanderobs:read, obs:incident, timeline:read, timeline:write, evidence:read, evidence:create, attest:read
Issuer Directoryrole/issuer-directory-viewerissuer-directory:read
Issuer Directoryrole/issuer-directory-operatorissuer-directory:read, issuer-directory:write
Issuer Directoryrole/issuer-directory-adminissuer-directory:read, issuer-directory:write, issuer-directory:admin
Task Packsrole/packs-viewerpacks.read
Task Packsrole/packs-operatorpacks.read, packs.run
Task Packsrole/packs-publisherpacks.read, packs.write
Task Packsrole/packs-approverpacks.read, packs.approve
Airgaprole/airgap-viewerairgap:status:read
Airgaprole/airgap-operatorairgap:status:read, airgap:import
Airgaprole/airgap-adminairgap:status:read, airgap:import, airgap:seal
Exceptionsrole/exceptions-viewerexceptions:read
Exceptionsrole/exceptions-approverexceptions:read, exceptions:approve
Exceptionsrole/exceptions-editorexceptions:read, exceptions:write
Attestorrole/attestor-viewerattest:read, aoc:verify
Attestorrole/attestor-operatorattest:read, attest:create, aoc:verify
Attestorrole/attestor-adminattest:read, attest:create, attest:admin, aoc:verify
Signerrole/signer-viewersigner:read, aoc:verify
Signerrole/signer-operatorsigner:read, signer:sign, aoc:verify
Signerrole/signer-adminsigner:read, signer:sign, signer:rotate, signer:admin, aoc:verify
SBOMrole/sbom-viewersbom:read, aoc:verify
SBOMrole/sbom-creatorsbom:read, sbom:write, aoc:verify
SBOMrole/sbom-attestorsbom:read, sbom:write, sbom:attest, attest:create, aoc:verify
Releaserole/release-viewerrelease:read, policy:read, findings:read
Releaserole/service-release-contributorui.read, ui.preferences.read, ui.preferences.write, release:read, release:write, orch:read, scheduler:read, scanner:read, scanner:scan, sbom:read, policy:read, findings:read, evidence:read, vuln:view, timeline:read, platform.context.read
Releaserole/release-developerLegacy alias only; migrate users to role/service-release-contributor.
Releaserole/release-managerrelease:read, release:write, policy:read, findings:read
Releaserole/release-publisherrelease:read, release:write, release:publish, policy:read, findings:read
Releaserole/release-adminrelease:read, release:write, release:publish, release:bypass, policy:read, findings:read
Zastavarole/zastava-viewerzastava:read
Zastavarole/zastava-operatorzastava:read, zastava:trigger
Zastavarole/zastava-adminzastava:read, zastava:trigger, zastava:admin

Scope availability (reconciled against StellaOpsScopes.cs):

This subsection previously tracked scopes that “must be added to Authority”. As of this reconciliation, every scope referenced in the §4.3 catalog is already defined as a const in src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOpsScopes.cs, so the catalog rows describe enforceable scopes — what remains aspirational is the seeding of most module role bundles (see the seeded-vs-proposed note at the top of §4.3), not the scopes themselves.

Confirmed present in the catalog (was previously listed as “missing/new”):

Caveat (gateway enforcement vs. scope existence): a scope being defined in StellaOpsScopes.cs only means an issued OpTok may carry it (token issuance filters requested scopes against authority.clients.allowed_scopes; the authority.permissions RBAC catalog is not consulted at issuance — see the class remarks in StellaOpsScopes.cs). It does not by itself guarantee that every downstream service enforces the scope on its endpoints; verify per-service authorization policies against the owning module when that guarantee matters.

5. Console Admin API Surface

5.1 Tenants

Scopes: authority:tenants.read (read/list/operator-compliance read), authority:tenants.write (create/update/suspend/resume and all operator-compliance mutations).

Suspension has an additional invariant beyond scope and fresh-auth checks: the normalized target tenant must differ from the tenant resolved by TenantHeaderFilter for the authenticated request. Attempting to suspend the active context returns sealed HTTP 409 application/problem+json with code active_tenant_suspend_forbidden and performs no repository update, shared-tenant propagation, or success audit. An operator must switch to a different tenant context before suspending the former tenant.

5.2 Users

Legacy alias (same handlers, same scopes/filters): GET /api/admin/users and POST /api/admin/users. The 201 Location header reflects whichever prefix was called (/api/admin/users/{id} vs /console/admin/users/{id}).

Scopes: authority:users.read (list), authority:users.write (create/update/disable/enable).

POST /console/admin/users requires a non-blank password for local users. Missing or blank passwords return HTTP 400 with error=password_required and code=PASSWORD_REQUIRED so QA and clients can assert the exact contract. The created account is hashed with Argon2id and a authority.admin.users.password.set audit row is emitted alongside authority.admin.users.create. DELETE of the last enabled user in a tenant is rejected with error=last_admin_user (HTTP 400) to avoid lockout.

5.3 Roles and scopes

Scopes: authority:roles.read|write

5.4 Clients

Scopes: authority:clients.read|write

5.5 Tokens and audit

Scopes: authority:tokens.read (list), authority:tokens.revoke (revoke), authority.audit.read (audit list — dot-form scope, not authority:audit.read).

Both token endpoints were stubs until 2026-07-21 (the list always returned [] + "implementation pending", the revoke echoed the request count without revoking — Fable audit #12). They now run against the real IAuthorityTokenStore (authority.oidc_tokens, the same store issuance persists into and TokenValidationHandlers consults):

Deprecation: GET /console/admin/audit carries a Sunset header (2027-10-19) and advertises a successor link to the unified Timeline audit store at /api/v1/audit/events?modules=authority (DeprecatedForTimeline(...) in ConsoleAdminEndpointExtensions). New integrations should read audit from Timeline rather than this endpoint.

5.6 Branding

Scopes: authority:branding.read (admin GET / preview), authority:branding.write (PUT). Source: ConsoleBrandingEndpointExtensions.

6. Audit and Observability

7. Offline-first Administration

NOT IMPLEMENTED / roadmap. This section describes a planned signed-bundle import workflow. As of this reconciliation there is no /admin/bundles/apply endpoint (or any /admin/bundles* route) in the Authority source — searching src/** returns no match. The Console admin surface (/console/admin/*, /api/admin/users) operates against a live Authority instance over bearer scopes; there is no offline change-manifest apply path in code yet. Keep the design below as the intended direction, not as shipped behavior.

8. UI Integration Contract

9. References