Web Gateway Tenant RBAC Contract
Contract ID: CONTRACT-GATEWAY-RBAC-001 Status: APPROVED (reconciled against implementation 2026-05-30) Effective Date: 2025-12-07 Owners: Gateway Guild, Authority Guild, Web UI Guild
Reconciliation note (2026-05-30): This contract was authored ahead of implementation and has been reconciled against the shipping gateway and Authority code. Sections that described a planned org/tenant/project role hierarchy and OAuth scope-inheritance model that were never built are now marked NOT IMPLEMENTED with the actual behaviour documented in their place. Ground-truth sources:
src/Router/StellaOps.Gateway.WebService(gateway authz/identity middleware),src/Authority(Authority scopes/roles), and the canonical scope catalogsrc/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOpsScopes.cs.
Overview
This contract defines the tenant isolation and role-based access control (RBAC) model for the StellaOps Web Gateway, ensuring consistent authorization across all API endpoints and UI components.
The shipping authorization model is flat, scope-based, and tenant-scoped:
- The gateway (
StellaOps.Gateway.WebService) authorizes each request by matching the principal’s OAuth scopes against per-endpoint claim requirements. There is no role hierarchy and no scope inheritance — every required scope must be present explicitly (AuthorizationMiddleware.HasRequiredScope). - Tenancy is carried in the JWT only and propagated to backends via gateway-stripped-and-reissued identity headers plus a signed identity envelope (
IdentityHeaderPolicyMiddleware). - Roles are arbitrary named, tenant-scoped entities stored in Authority (the only auto-seeded role is
admin); a role is just a named bundle of scope permissions, not a fixed tier.
Tenant Model
Tenant Hierarchy
NOT IMPLEMENTED (aspirational). The code has no Organization (Org) entity and no Project hierarchy enforced at the gateway. Tenants are a flat namespace. A single optional
stellaops:projectclaim exists for in-tenant project scoping (StellaOpsClaimTypes.Project), but there is no Org layer above tenants. The diagram below is retained as a future/roadmap target only.
(roadmap target — not enforced)
Organization (Org)
├── Tenant A
│ ├── Project 1
│ │ └── Resources...
│ └── Project 2
│ └── Resources...
└── Tenant B
└── Project 3
└── Resources...
Tenant Identification
Tenants are resolved exclusively from validated JWT claims. Client-supplied tenant headers (X-StellaOps-TenantId, X-StellaOps-Tenant, X-Stella-Tenant, X-Tenant-Id) are stripped at gateway ingress to prevent spoofing and are not honoured as a tenancy source (IdentityHeaderPolicyMiddleware.ReservedHeaders, StellaOpsTenantResolver remarks: “this resolver reads the tenant identity exclusively from envelope-bound JWT claims”).
The tenant claim is read in this priority order (StellaOpsTenantResolver.TryResolveTenant):
- Canonical claim:
stellaops:tenant - Alias claim:
tenant_id - Alias claim:
tenant - Legacy claim:
tid
If none is present, resolution fails with the machine-readable error tenant_missing.
Service-to-service / header note: After the gateway resolves identity it writes the downstream headers
X-StellaOps-TenantIdandX-Tenant-Idfor backend consumption, but backends do not treat those headers as an authoritative tenancy source — they re-resolve from the signed identity envelope claims. There is no/tenants/{tenantId}/...path-parameter tenant resolution in the gateway; path-based tenant selection is NOT IMPLEMENTED.
Tenant Override (per-request tenant selection)
For principals whose token carries an allow-list of tenants (stellaops:allowed_tenants), the gateway can honour a client-requested tenant header override only when explicitly enabled (IdentityHeaderPolicyOptions.EnableTenantOverride, default false). The requested tenant must be present in the principal’s stellaops:allowed_tenants allow-list; otherwise the gateway returns 403 with body { "error": "tenant_override_forbidden", ... } (IdentityHeaderPolicyMiddleware.TryApplyTenantOverride).
Role Definitions
NOT IMPLEMENTED as written. The fixed role catalog and role hierarchy below (
org:admin,tenant:admin,project:*,policy:admin,scanner:operator,airgap:admin, …) do not exist in the codebase. None of these role names appear insrc/Authority. They are retained below only as a strikethrough record of the original design; the Actual role model subsection that follows describes shipping behaviour.
Actual role model (implemented)
Roles in StellaOps are arbitrary named, tenant-scoped entities persisted in Authority (RoleEntity, managed via IRoleRepository). A role is simply a named bundle of scope permissions assigned to it; there is no fixed tier hierarchy and no inheritance between roles.
- The only role auto-seeded at bootstrap is
admin(display name “Administrator”, “Full platform access”), created byStandardPluginBootstrapper.EnsureAdminRoleAsync. It is granted the full scope set (StellaOpsScopes.All). The default bootstrap user (admin) is assigned this role (etc/authority/plugins/standard.yaml→roles: ["admin"]). - Operators assemble additional custom roles through the Console Admin role editor (
ConsoleAdminEndpointExtensions.ListRoles/CreateRole/UpdateRole), choosing from the scopes catalogued in theauthority.permissionstable (seeded by migrationS002_seed_full_scope_permissions.sqlto mirrorStellaOpsScopes.All). - Roles are surfaced on the principal as ASP.NET
ClaimTypes.Roleclaims; the gateway forwards them in the signed identity envelope (Rolesfield) but authorization decisions are made on scopes, not role names (AuthorizationMiddleware).
Original (NOT IMPLEMENTED) built-in role catalog and hierarchy — retained for reference
Built-in Roles
org:admin | ||
org:reader | ||
tenant:admin | ||
tenant:operator | ||
tenant:viewer | ||
project:admin | ||
project:contributor | ||
project:viewer | ||
policy:admin | ||
scanner:operator | ||
airgap:admin |
Role Hierarchy (not enforced):
(NOT IMPLEMENTED — no role hierarchy exists in code)
org:admin
├── org:reader
├── tenant:admin
│ ├── tenant:operator
│ │ └── tenant:viewer
│ ├── policy:admin
│ ├── scanner:operator
│ └── airgap:admin
└── project:admin
├── project:contributor
└── project:viewer
Scopes
OAuth 2.0 Scopes
The canonical scope catalog is src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOpsScopes.cs. Only scopes registered there can appear in an issued token’s scope claim (token issuance filters requested scopes against authority.clients.allowed_scopes). The table below lists the scopes most relevant to this contract, using their actual catalog names. Scopes are not tied to a “required role” in code — authorization is a flat scope match, so the former “Required Role” column has been removed.
| Scope (catalog const) | Description |
|---|---|
policy:read (PolicyRead) | Read policy metadata |
policy:edit (PolicyEdit) | Edit policy configurations |
policy:write (PolicyWrite) | Create/edit policy drafts |
policy:activate (PolicyActivate) | Activate policies |
scanner:read (ScannerRead) | View scan results and metadata |
scanner:scan (ScannerScan) | Trigger Scanner scan operations |
scanner:export (ScannerExport) | Export Scanner results (SBOM, reports) |
airgap:seal (AirgapSeal) | Seal/unseal an installation (air-gapped mode) |
airgap:import (AirgapImport) | Import offline bundles while sealed |
airgap:status:read (AirgapStatusRead) | Read air-gap status / sealing state |
export.viewer (ExportViewer) | Read export center runs and bundles |
export.operator (ExportOperator) | Operate export scheduling and run execution |
export.admin (ExportAdmin) | Administer export retention, keys, scheduling |
authority:users.read (AuthorityUsersRead) | Read Authority user management |
authority:users.write (AuthorityUsersWrite) | Write Authority user management |
authority:roles.read / authority:roles.write | Read / write Authority role management |
ui.admin (UiAdmin) | Console Admin UI and workflows |
Corrections vs. the original draft (NOT IN CATALOG): the original table listed
scanner:execute,airgap:verify,export:read,export:create,admin:users, andadmin:settings. None of those scope names exist inStellaOpsScopes.cs. Their nearest real equivalents arescanner:scan, (noairgap:verify— air-gap surface isairgap:seal/airgap:import/airgap:status:read),export.viewer/export.operator, and theauthority:users.read/authority:users.write/ui.adminscopes respectively.policy:read,policy:edit,policy:activate,scanner:read,airgap:seal, andairgap:status:readare correct as written.
Scope Inheritance
NOT IMPLEMENTED. There is no scope-inheritance / parent-child scope mechanism in the codebase. The gateway performs an exact-match scope check per required scope (
AuthorizationMiddleware.HasRequiredScope— a scope is granted only if the resolved scope set contains it verbatim or the space-separatedscope/scpclaim contains it verbatim). Noscope_inheritance,HasInheritedScope, or implied-scope lookup exists. A token must carry every scope it needs; grantingpolicy:editdoes not implicitly grantpolicy:read. The YAML below is retained only as a record of the original (unimplemented) design.
# NOT IMPLEMENTED — no parent→child scope expansion exists in code.
scope_inheritance:
"policy:edit": ["policy:read"]
"policy:activate": ["policy:read", "policy:edit"]
"scanner:execute": ["scanner:read"] # note: scanner:execute is also not a real scope
"export:create": ["export:read"] # note: neither scope name exists
"admin:users": ["admin:settings"] # note: neither scope name exists
One related real mechanism (different from inheritance): the gateway does perform a small, hard-coded coarse→fine scope expansion for a handful of legacy scopes (
IdentityHeaderPolicyMiddleware.ExpandCoarseScopes):scheduler:read→scheduler.schedules.read+scheduler.runs.read;scheduler:operate→scheduler.schedules.write+scheduler.runs.write+scheduler.runs.preview+scheduler.runs.manage; andorch:quota→quota.read+quota.admin. This is a fixed bridging map, not a generic read/write inheritance model.
Resource Authorization
Resource Types
| Resource Type | Tenant Scoped | Project Scoped | Description |
|---|---|---|---|
risk_profile | Yes | No | Risk scoring profiles |
policy_pack | Yes | No | Policy bundles |
scan_result | Yes | Yes | Scan outputs |
export | Yes | Yes | Export jobs |
finding | Yes | Yes | Vulnerability findings |
vex_document | Yes | Yes | VEX statements |
sealed_mode | Yes | No | Air-gap state |
user | Yes | No | Tenant users |
project | Yes | No | Projects |
Authorization Rules
Illustrative, NOT a loaded artifact. There is no
authorization-rules.yamlfile consumed by the gateway. Per-endpoint claim requirements are supplied by each service’s endpoint metadata and resolved at request time byIEffectiveClaimsStore.GetEffectiveClaims(...); the gateway then matches them with an exact scope check. The YAML below illustrates intent only. Note the corrections:required_scopes: [scanner:execute]should read[scanner:scan](and Scanner has no per-resourcedeletescope in the catalog), andrequire_role:is NOT enforced — the gateway never checks role names, only scopes.tenant_isolationandproject_isolationkeys below are descriptive: tenancy is enforced by each service via the resolved tenant claim, not by gateway-level rule keys.
# ILLUSTRATIVE ONLY — not loaded by the gateway; require_role is not enforced.
rules:
- resource: risk_profile
actions:
read:
required_scopes: [policy:read]
tenant_isolation: strict
create:
required_scopes: [policy:edit]
tenant_isolation: strict
update:
required_scopes: [policy:edit]
tenant_isolation: strict
activate:
required_scopes: [policy:activate]
tenant_isolation: strict
delete:
required_scopes: [policy:edit]
tenant_isolation: strict
# require_role: policy:admin # NOT ENFORCED — no role check in gateway
- resource: scan_result
actions:
read:
required_scopes: [scanner:read]
tenant_isolation: strict
project_isolation: optional
create:
required_scopes: [scanner:scan] # corrected from non-existent scanner:execute
tenant_isolation: strict
delete:
required_scopes: [scanner:scan] # corrected; no dedicated scanner delete scope exists
tenant_isolation: strict
# require_role: scanner:operator # NOT ENFORCED — no role check in gateway
- resource: sealed_mode
actions:
read:
required_scopes: [airgap:status:read]
tenant_isolation: strict
seal:
required_scopes: [airgap:seal]
tenant_isolation: strict
# require_role: airgap:admin # NOT ENFORCED — no role check in gateway
audit: required
unseal:
required_scopes: [airgap:seal] # seal scope also gates unseal (no separate scope)
tenant_isolation: strict
# require_role: airgap:admin # NOT ENFORCED — no role check in gateway
audit: required
Tenant Isolation
Strict Isolation
All data access is tenant-scoped by default:
-- Example: All queries include tenant filter
SELECT * FROM findings
WHERE tenant_id = @current_tenant_id
AND deleted_at IS NULL;
Cross-Tenant Access
A token is bound to the single tenant in its stellaops:tenant claim. The only cross-tenant mechanism that exists in code is the stellaops:allowed_tenants allow-list combined with the gateway tenant-override feature:
- A principal whose token carries
stellaops:allowed_tenants(a delimited list) may select any tenant in that list by supplying a tenant header, but only whenEnableTenantOverrideis turned on (default off). The override is validated against the allow-list and rejected with403 tenant_override_forbiddenotherwise (IdentityHeaderPolicyMiddleware.ResolveAllowedTenants/TryApplyTenantOverride). - Multi-tenant service/client_credentials clients (e.g. the cross-tenant release-dispatch operator client) are configured with a plural
tenantslist inetc/authority/plugins/standard.yaml, and Authority mints a token whosestellaops:tenantis set to the requested tenant per request.
NOT IMPLEMENTED: there is no Organization-admin “access all tenants in their org” capability, no
cross_tenantscope, and noorg:readeraggregation role — none of these exist in the codebase. Cross-tenant reach is governed solely by thestellaops:allowed_tenantsallow-list above.
Isolation Enforcement Points
| Layer | Enforcement |
|---|---|
| Gateway | Resolves tenant from validated JWT claim; strips client tenant headers and re-issues X-StellaOps-TenantId / X-Tenant-Id + signed identity envelope (IdentityHeaderPolicyMiddleware) |
| Service | Re-resolves tenant from envelope claims (StellaOpsTenantResolver) and applies a tenant filter to queries; RequireTenant() endpoint filters reject requests with no resolved tenant |
| Database | Row-level security (RLS) policies on schemas that opt in (e.g. Authority initial schema, Attestor trust verdicts); not universally applied across all module schemas |
| Cache | Tenant-prefixed cache keys (per-service convention) |
JWT Claims
Representative Claims
Claim names below are grounded in StellaOpsClaimTypes. Roles are emitted as standard ASP.NET role claims (ClaimTypes.Role), not under a stellaops:roles claim. The tenant is normally a slug/identifier (e.g. default), not necessarily a UUID.
{
"sub": "user-uuid",
"aud": ["stellaops-api"],
"iss": "https://stella-ops.local",
"exp": 1701936000,
"iat": 1701932400,
"stellaops:tenant": "default",
"stellaops:allowed_tenants": "default tenant-golden-local",
"stellaops:project": "*",
"role": ["admin"],
"scope": "policy:read policy:edit scanner:read"
}
Custom Claims
Claim (StellaOpsClaimTypes) | Type | Description |
|---|---|---|
stellaops:tenant (Tenant) | string | Current tenant identifier (canonical tenancy claim) |
stellaops:allowed_tenants (AllowedTenants) | string | Space/comma/semicolon-delimited allow-list of selectable tenants (enables tenant override) |
stellaops:project (Project) | string | Optional in-tenant project scope (* = any project, StellaOpsTenancyDefaults.AnyProject) |
scope (Scope) / scp (ScopeItem) | string / string[] | Space-separated scope list / individual scope items |
client_id (ClientId) | string | OAuth2 client identifier |
stellaops:idp (IdentityProvider) | string | Identity provider hint for downstream services |
NOT IMPLEMENTED claims: the original draft listed
stellaops:org,stellaops:roles,stellaops:projects, andstellaops:tier. None of these claim types exist inStellaOpsClaimTypes. There is no organization claim; roles are carried via the standardroleclaim; project scoping is a singlestellaops:projectvalue (not an array); and there is nostellaops:tierrate-limit-tier claim. (For other genuine claim types — operator-reason/ticket, policy publish/promote attestation claims, vuln ABAC filtersstellaops:attr:env|owner|business_tier,sid,jti,cnf— seeStellaOpsClaimTypes.)
Gateway Implementation
The snippets below reflect the actual middleware classes shipped in
src/Router/StellaOps.Gateway.WebService. (There is noTenantAuthorizationMiddlewareorScopeAuthorization.RequireScope/HasInheritedScope— those names in the original draft were placeholders.)
Identity + Tenant Resolution (IdentityHeaderPolicyMiddleware)
IdentityHeaderPolicyMiddleware strips spoofable identity headers, extracts the validated principal, resolves the tenant from claims, optionally applies a tenant override against the stellaops:allowed_tenants allow-list, then writes downstream identity headers and a signed identity envelope:
// IdentityHeaderPolicyMiddleware.InvokeAsync (abridged)
StripReservedHeaders(context, ShouldPreserveAuthHeaders(context.Request.Path));
var identity = ExtractIdentity(context); // tenant from stellaops:tenant / tenant_id / tenant / tid
if (!identity.IsAnonymous && requestedTenant != identity.Tenant &&
!TryApplyTenantOverride(context, identity, requestedTenant)) // checks stellaops:allowed_tenants
{
await context.Response.WriteAsJsonAsync(new { error = "tenant_override_forbidden", ... });
return;
}
StoreIdentityContext(context, identity); // Gateway.TenantId, Gateway.Scopes, ...
WriteDownstreamHeaders(context, identity); // X-StellaOps-TenantId, X-Tenant-Id, X-StellaOps-Scopes, envelope
Scope Authorization (AuthorizationMiddleware)
AuthorizationMiddleware resolves per-endpoint claim requirements (IEffectiveClaimsStore) and enforces them with an exact scope match — no inheritance:
// AuthorizationMiddleware.HasRequiredScope (abridged) — exact match only
private static bool HasRequiredScope(IEnumerable<Claim> userClaims,
ISet<string>? resolvedScopes, string requiredScope)
{
if (resolvedScopes is not null && resolvedScopes.Contains(requiredScope))
return true;
// scope/scp claim may be space-separated (RFC 6749 §3.3) or individual claims
return userClaims.Any(c =>
(c.Type is "scope" or "scp") &&
(c.Value == requiredScope ||
c.Value.Split(' ', StringSplitOptions.RemoveEmptyEntries)
.Any(s => string.Equals(s, requiredScope, StringComparison.Ordinal))));
}
On failure the gateway returns 403 (or 401 when unauthenticated) with the AuthorizationFailureResponse body described under Error Responses below. Note the asymmetric-scope guard: a state-changing method (POST/PUT/PATCH/DELETE) that declares no claim requirements of its own inherits the union of its sibling read-method scope requirements so an unauthorized write returns 403 rather than reaching body validation (AuthorizationMiddleware.InvokeAsync).
Web UI Integration
The Angular console gates UI access on scopes, not role names (src/Web/StellaOps.Web/src/app/core/auth/). AuthService exposes hasScope(scope), hasAnyScope(scopes), and hasAllScopes(scopes) over the session’s current scope set (AuthService.scopes()), with the special admin scope treated as a superuser short-circuit (hasScope returns true when the user holds admin; see scopes.ts). There is no hasAnyRole / role-based guard — the original draft’s role-keyed guard does not exist.
Route Guards (scope-based)
// Real pattern: guards/UI gates use scope checks (AuthService.hasScope / hasAnyScope).
// e.g. AuthService convenience getters:
// canOperateOrchestrator() => hasAdminPrivilege() || hasScope(ORCH_OPERATE)
// canEditPolicy() => hasScope(POLICY_EDIT)
export const ScopeGuard: CanActivateFn = (route) => {
const auth = inject(AuthService);
const requiredScopes = route.data['scopes'] as StellaOpsScope[];
if (!auth.hasAnyScope(requiredScopes)) {
return inject(Router).createUrlTree(['/unauthorized']);
}
return true;
};
// Usage in routes (scope-keyed, NOT role-keyed)
{
path: 'policy/studio',
component: PolicyStudioComponent,
canActivate: [ScopeGuard],
data: { scopes: ['policy:author', 'policy:edit'] }
}
Scope-Based UI Elements
The console hides/show elements based on AuthService.hasScope(...). (The exact structural-directive name is illustrative; the load-bearing call is the scope check.)
// Illustrative directive — gates on a real scope via AuthService.hasScope
@Directive({ selector: '[requireScope]' })
export class RequireScopeDirective {
@Input() set requireScope(scope: StellaOpsScope) {
this.viewContainer.clear();
if (this.auth.hasScope(scope)) {
this.viewContainer.createEmbeddedView(this.templateRef);
}
}
}
// Usage in templates
<button *requireScope="'policy:activate'">Activate Policy</button>
Audit Trail
Illustrative shape. The JSON below is an example audit envelope, not a verbatim schema emitted by a single component.
roleshere would be the principal’s role-claim values (e.g.["admin"]), not the nonexistentpolicy:adminrole.
Audited Operations
Write operations are logged with an envelope along these lines:
{
"timestamp": "2025-12-07T10:30:00Z",
"actor": {
"userId": "user-uuid",
"tenantId": "default",
"roles": ["admin"],
"ipAddress": "192.168.1.100"
},
"action": "policy.activate",
"resource": {
"type": "policy_pack",
"id": "pack-123",
"version": 5
},
"outcome": "success",
"details": {
"previousStatus": "approved",
"newStatus": "active"
}
}
Sensitive Operations
These operations require enhanced audit logging:
sealed_mode.seal/sealed_mode.unsealpolicy.activateexport.create(with PII)user.role.assigntenant.settings.modify
Configuration
Authority role / scope configuration (actual)
NOT IMPLEMENTED: there is no
gateway/rbac.yamlfile and noroleBindings/defaultRole/allowCrossTenantForOrgAdminconfig in the gateway. Roles and their scope grants are configured in Authority, not the gateway. The original RBAC-yaml block (withtenant:admin/policy:adminroles and thescanner:execute/export:read/export:create/admin:users/admin:settingsscopes) referenced entities that do not exist.
Role/scope binding is defined per-client/bootstrap in etc/authority/plugins/standard.yaml. For example, the default admin bootstrap declares roles: ["admin"], and clients declare an allowedScopes list that caps which catalog scopes the issued token may carry. A representative slice (real scope names):
# etc/authority/plugins/standard.yaml (excerpt — real shape)
tenantId: "default"
defaultAdminBootstrap:
username: "admin"
passwordEnvironmentVariable: "STELLAOPS_ADMIN_PASS"
roles:
- "admin" # the only auto-seeded role; granted StellaOpsScopes.All
clients:
- clientId: "..."
tenants: "default tenant-golden-local"
allowedScopes: >-
ui.read ui.admin authority:users.read authority:users.write
authority:roles.read authority:roles.write policy:read policy:edit
scanner:read scanner:write export.viewer export.operator export.admin
airgap:status:read ...
Authorization is then enforced at the gateway purely on the scopes present in the issued token (no role binding is consulted at request time; authority.clients.allowed_scopes is the issuance-time filter — see StellaOpsScopes.cs remarks).
Error Responses
The gateway authz failure body is
AuthorizationFailureResponse(camelCase), not an RFC 7807problems/*document. The fields areerror,message,requiredClaimType,requiredClaimValue,service,version(AuthorizationMiddleware.WriteUnauthorizedAsync/WriteForbiddenAsync). The original draft’stype/title/detail/requiredScope/currentScopesshape was not implemented at the gateway. (Downstream service bodies — e.g.RouteDispatchMiddleware/AspNetRouterRequestDispatchererror mapping — may emit RFC 7807 problem documents for 404/500, but the gateway authz responses use the shape below.)
401 Unauthorized (gateway authz)
{
"error": "unauthorized",
"message": "Authentication required",
"requiredClaimType": "",
"requiredClaimValue": null,
"service": "<service-name>",
"version": "<endpoint-version>"
}
403 Forbidden (gateway authz)
{
"error": "forbidden",
"message": "Authorization failed: missing required claim",
"requiredClaimType": "scope",
"requiredClaimValue": "policy:activate",
"service": "<service-name>",
"version": "<endpoint-version>"
}
For a rejected per-request tenant override the gateway returns 403 with:
{
"error": "tenant_override_forbidden",
"message": "Requested tenant override is not permitted for this principal."
}
404 Not Found
The dispatcher returns 404 when no matching backend endpoint is found (AspNetRouterRequestDispatcher.CreateNotFoundResponse).
Caveat (not verified in code): the original claim that “404 is returned instead of 403 for resources in other tenants to prevent enumeration attacks” could not be confirmed as a gateway-level behaviour — the gateway returns
403on a failed scope/claim check, and tenant isolation is applied per-service at query time. Any 404-for-cross-tenant masking would be a per-service convention, not a gateway guarantee. Treat this as unverified / aspirational until a service implements it explicitly.
Changelog
| Date | Version | Change |
|---|---|---|
| 2025-12-07 | 1.0.0 | Initial contract definition |
| 2026-05-30 | 1.1.0 | Reconciled against shipping implementation. Marked the org/tenant/project role hierarchy, scope inheritance, require_role rules, gateway/rbac.yaml, and stellaops:org/stellaops:roles/stellaops:projects/stellaops:tier claims as NOT IMPLEMENTED. Corrected scope names (scanner:execute→scanner:scan, export:read/create→export.viewer/operator, admin:users/settings→authority:users.*/ui.admin). Documented the real flat scope-based authz (AuthorizationMiddleware), claim-only tenant resolution + stellaops:allowed_tenants override (IdentityHeaderPolicyMiddleware/StellaOpsTenantResolver), scope-based UI guards, and the actual AuthorizationFailureResponse error shape. |
References
- Authority Scopes Documentation — full scope catalog and descriptions.
- Scopes and Roles — RBAC model overview.
- Tenancy Overview
- Rate Limit Design
- Canonical scope catalog (source of truth):
src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOpsScopes.cs - Gateway authz/identity middleware:
src/Router/StellaOps.Gateway.WebService/Authorization/AuthorizationMiddleware.cs,.../Middleware/IdentityHeaderPolicyMiddleware.cs,.../Middleware/TenantMiddleware.cs - Tenant resolver:
src/Authority/StellaOps.Authority/StellaOps.Auth.ServerIntegration/Tenancy/StellaOpsTenantResolver.cs
