feat: Implement Scheduler Worker Options and Planner Loop

- Added `SchedulerWorkerOptions` class to encapsulate configuration for the scheduler worker.
- Introduced `PlannerBackgroundService` to manage the planner loop, fetching and processing planning runs.
- Created `PlannerExecutionService` to handle the execution logic for planning runs, including impact targeting and run persistence.
- Developed `PlannerExecutionResult` and `PlannerExecutionStatus` to standardize execution outcomes.
- Implemented validation logic within `SchedulerWorkerOptions` to ensure proper configuration.
- Added documentation for the planner loop and impact targeting features.
- Established health check endpoints and authentication mechanisms for the Signals service.
- Created unit tests for the Signals API to ensure proper functionality and response handling.
- Configured options for authority integration and fallback authentication methods.
This commit is contained in:
master
2025-10-27 09:46:31 +02:00
parent 799f787de2
commit 14617e9c3b
135 changed files with 10721 additions and 946 deletions

View File

@@ -14,4 +14,9 @@ public static class StellaOpsServiceIdentities
/// Service identity used by Cartographer when constructing and maintaining graph projections.
/// </summary>
public const string Cartographer = "cartographer";
/// <summary>
/// Service identity used by Vuln Explorer when issuing scoped permalink requests.
/// </summary>
public const string VulnExplorer = "vuln-explorer";
}

View File

@@ -6,6 +6,7 @@ using System.Security.Claims;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text.Json;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.Configuration;
@@ -389,6 +390,106 @@ public class ClientCredentialsHandlersTests
Assert.Equal("tenant-default", tenant);
}
[Fact]
public async Task ValidateClientCredentials_RejectsAdvisoryScopes_WhenTenantMissing()
{
var clientDocument = CreateClient(
clientId: "concelier-ingestor",
secret: "s3cr3t!",
allowedGrantTypes: "client_credentials",
allowedScopes: "advisory:ingest advisory:read");
var registry = CreateRegistry(withClientProvisioning: true, clientDescriptor: CreateDescriptor(clientDocument));
var options = TestHelpers.CreateAuthorityOptions();
var handler = new ValidateClientCredentialsHandler(
new TestClientStore(clientDocument),
registry,
TestActivitySource,
new TestAuthEventSink(),
new TestRateLimiterMetadataAccessor(),
TimeProvider.System,
new NoopCertificateValidator(),
new HttpContextAccessor(),
options,
NullLogger<ValidateClientCredentialsHandler>.Instance);
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "advisory:ingest");
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await handler.HandleAsync(context);
Assert.True(context.IsRejected);
Assert.Equal(OpenIddictConstants.Errors.InvalidClient, context.Error);
Assert.Equal("Advisory scopes require a tenant assignment.", context.ErrorDescription);
}
[Fact]
public async Task ValidateClientCredentials_RejectsVexScopes_WhenTenantMissing()
{
var clientDocument = CreateClient(
clientId: "excitor-ingestor",
secret: "s3cr3t!",
allowedGrantTypes: "client_credentials",
allowedScopes: "vex:ingest vex:read");
var registry = CreateRegistry(withClientProvisioning: true, clientDescriptor: CreateDescriptor(clientDocument));
var options = TestHelpers.CreateAuthorityOptions();
var handler = new ValidateClientCredentialsHandler(
new TestClientStore(clientDocument),
registry,
TestActivitySource,
new TestAuthEventSink(),
new TestRateLimiterMetadataAccessor(),
TimeProvider.System,
new NoopCertificateValidator(),
new HttpContextAccessor(),
options,
NullLogger<ValidateClientCredentialsHandler>.Instance);
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "vex:read");
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await handler.HandleAsync(context);
Assert.True(context.IsRejected);
Assert.Equal(OpenIddictConstants.Errors.InvalidClient, context.Error);
Assert.Equal("VEX scopes require a tenant assignment.", context.ErrorDescription);
}
[Fact]
public async Task ValidateClientCredentials_AllowsAdvisoryScopes_WithTenant()
{
var clientDocument = CreateClient(
clientId: "concelier-ingestor",
secret: "s3cr3t!",
allowedGrantTypes: "client_credentials",
allowedScopes: "advisory:ingest advisory:read",
tenant: "tenant-default");
var registry = CreateRegistry(withClientProvisioning: true, clientDescriptor: CreateDescriptor(clientDocument));
var options = TestHelpers.CreateAuthorityOptions();
var handler = new ValidateClientCredentialsHandler(
new TestClientStore(clientDocument),
registry,
TestActivitySource,
new TestAuthEventSink(),
new TestRateLimiterMetadataAccessor(),
TimeProvider.System,
new NoopCertificateValidator(),
new HttpContextAccessor(),
options,
NullLogger<ValidateClientCredentialsHandler>.Instance);
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "advisory:read");
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await handler.HandleAsync(context);
Assert.False(context.IsRejected, $"Rejected: {context.Error} - {context.ErrorDescription}");
var grantedScopes = Assert.IsType<string[]>(context.Transaction.Properties[AuthorityOpenIddictConstants.ClientGrantedScopesProperty]);
Assert.Equal(new[] { "advisory:read" }, grantedScopes);
}
[Fact]
public async Task ValidateClientCredentials_AllowsGraphWrite_ForCartographerServiceIdentity()
{
@@ -992,6 +1093,206 @@ public class TokenValidationHandlersTests
Assert.Equal(OpenIddictConstants.Errors.InvalidToken, context.Error);
}
[Fact]
public async Task ValidateAccessTokenHandler_AddsTenantClaim_FromTokenDocument()
{
var clientDocument = CreateClient(tenant: "tenant-alpha");
var tokenStore = new TestTokenStore
{
Inserted = new AuthorityTokenDocument
{
TokenId = "token-tenant",
Status = "valid",
ClientId = clientDocument.ClientId,
Tenant = "tenant-alpha"
}
};
var metadataAccessor = new TestRateLimiterMetadataAccessor();
var auditSink = new TestAuthEventSink();
var sessionAccessor = new NullMongoSessionAccessor();
var handler = new ValidateAccessTokenHandler(
tokenStore,
sessionAccessor,
new TestClientStore(clientDocument),
CreateRegistry(withClientProvisioning: true, clientDescriptor: CreateDescriptor(clientDocument)),
metadataAccessor,
auditSink,
TimeProvider.System,
TestActivitySource,
NullLogger<ValidateAccessTokenHandler>.Instance);
var transaction = new OpenIddictServerTransaction
{
Options = new OpenIddictServerOptions(),
EndpointType = OpenIddictServerEndpointType.Token,
Request = new OpenIddictRequest()
};
var principal = CreatePrincipal(clientDocument.ClientId, "token-tenant", clientDocument.Plugin);
var context = new OpenIddictServerEvents.ValidateTokenContext(transaction)
{
Principal = principal,
TokenId = "token-tenant"
};
await handler.HandleAsync(context);
Assert.False(context.IsRejected);
Assert.Equal("tenant-alpha", principal.FindFirstValue(StellaOpsClaimTypes.Tenant));
Assert.Equal("tenant-alpha", metadataAccessor.GetMetadata()?.Tenant);
}
[Fact]
public async Task ValidateAccessTokenHandler_Rejects_WhenTenantDiffersFromToken()
{
var clientDocument = CreateClient(tenant: "tenant-alpha");
var tokenStore = new TestTokenStore
{
Inserted = new AuthorityTokenDocument
{
TokenId = "token-tenant",
Status = "valid",
ClientId = clientDocument.ClientId,
Tenant = "tenant-alpha"
}
};
var metadataAccessor = new TestRateLimiterMetadataAccessor();
var auditSink = new TestAuthEventSink();
var sessionAccessor = new NullMongoSessionAccessor();
var handler = new ValidateAccessTokenHandler(
tokenStore,
sessionAccessor,
new TestClientStore(clientDocument),
CreateRegistry(withClientProvisioning: true, clientDescriptor: CreateDescriptor(clientDocument)),
metadataAccessor,
auditSink,
TimeProvider.System,
TestActivitySource,
NullLogger<ValidateAccessTokenHandler>.Instance);
var transaction = new OpenIddictServerTransaction
{
Options = new OpenIddictServerOptions(),
EndpointType = OpenIddictServerEndpointType.Token,
Request = new OpenIddictRequest()
};
var principal = CreatePrincipal(clientDocument.ClientId, "token-tenant", clientDocument.Plugin);
principal.Identities.First().AddClaim(new Claim(StellaOpsClaimTypes.Tenant, "tenant-beta"));
var context = new OpenIddictServerEvents.ValidateTokenContext(transaction)
{
Principal = principal,
TokenId = "token-tenant"
};
await handler.HandleAsync(context);
Assert.True(context.IsRejected);
Assert.Equal(OpenIddictConstants.Errors.InvalidToken, context.Error);
Assert.Equal("The token tenant does not match the issued tenant.", context.ErrorDescription);
}
[Fact]
public async Task ValidateAccessTokenHandler_AssignsTenant_FromClientWhenTokenMissing()
{
var clientDocument = CreateClient(tenant: "tenant-alpha");
var tokenStore = new TestTokenStore
{
Inserted = new AuthorityTokenDocument
{
TokenId = "token-tenant",
Status = "valid",
ClientId = clientDocument.ClientId
}
};
var metadataAccessor = new TestRateLimiterMetadataAccessor();
var auditSink = new TestAuthEventSink();
var sessionAccessor = new NullMongoSessionAccessor();
var handler = new ValidateAccessTokenHandler(
tokenStore,
sessionAccessor,
new TestClientStore(clientDocument),
CreateRegistry(withClientProvisioning: true, clientDescriptor: CreateDescriptor(clientDocument)),
metadataAccessor,
auditSink,
TimeProvider.System,
TestActivitySource,
NullLogger<ValidateAccessTokenHandler>.Instance);
var transaction = new OpenIddictServerTransaction
{
Options = new OpenIddictServerOptions(),
EndpointType = OpenIddictServerEndpointType.Token,
Request = new OpenIddictRequest()
};
var principal = CreatePrincipal(clientDocument.ClientId, "token-tenant", clientDocument.Plugin);
var context = new OpenIddictServerEvents.ValidateTokenContext(transaction)
{
Principal = principal,
TokenId = "token-tenant"
};
await handler.HandleAsync(context);
Assert.False(context.IsRejected);
Assert.Equal("tenant-alpha", principal.FindFirstValue(StellaOpsClaimTypes.Tenant));
Assert.Equal("tenant-alpha", metadataAccessor.GetMetadata()?.Tenant);
}
[Fact]
public async Task ValidateAccessTokenHandler_Rejects_WhenClientTenantDiffers()
{
var clientDocument = CreateClient(tenant: "tenant-beta");
var tokenStore = new TestTokenStore
{
Inserted = new AuthorityTokenDocument
{
TokenId = "token-tenant",
Status = "valid",
ClientId = clientDocument.ClientId
}
};
var metadataAccessor = new TestRateLimiterMetadataAccessor();
var auditSink = new TestAuthEventSink();
var sessionAccessor = new NullMongoSessionAccessor();
var handler = new ValidateAccessTokenHandler(
tokenStore,
sessionAccessor,
new TestClientStore(clientDocument),
CreateRegistry(withClientProvisioning: true, clientDescriptor: CreateDescriptor(clientDocument)),
metadataAccessor,
auditSink,
TimeProvider.System,
TestActivitySource,
NullLogger<ValidateAccessTokenHandler>.Instance);
var transaction = new OpenIddictServerTransaction
{
Options = new OpenIddictServerOptions(),
EndpointType = OpenIddictServerEndpointType.Token,
Request = new OpenIddictRequest()
};
var principal = CreatePrincipal(clientDocument.ClientId, "token-tenant", clientDocument.Plugin);
principal.Identities.First().AddClaim(new Claim(StellaOpsClaimTypes.Tenant, "tenant-alpha"));
var context = new OpenIddictServerEvents.ValidateTokenContext(transaction)
{
Principal = principal,
TokenId = "token-tenant"
};
await handler.HandleAsync(context);
Assert.True(context.IsRejected);
Assert.Equal(OpenIddictConstants.Errors.InvalidToken, context.Error);
Assert.Equal("The token tenant does not match the registered client tenant.", context.ErrorDescription);
}
[Fact]
public async Task ValidateAccessTokenHandler_EnrichesClaims_WhenProviderAvailable()
{

View File

@@ -283,6 +283,11 @@ internal sealed class ValidateClientCredentialsHandler : IOpenIddictServerHandle
var hasGraphExport = grantedScopes.Length > 0 && Array.IndexOf(grantedScopes, StellaOpsScopes.GraphExport) >= 0;
var hasGraphSimulate = grantedScopes.Length > 0 && Array.IndexOf(grantedScopes, StellaOpsScopes.GraphSimulate) >= 0;
var graphScopesRequested = hasGraphRead || hasGraphWrite || hasGraphExport || hasGraphSimulate;
var hasAdvisoryIngest = grantedScopes.Length > 0 && Array.IndexOf(grantedScopes, StellaOpsScopes.AdvisoryIngest) >= 0;
var hasAdvisoryRead = grantedScopes.Length > 0 && Array.IndexOf(grantedScopes, StellaOpsScopes.AdvisoryRead) >= 0;
var hasVexIngest = grantedScopes.Length > 0 && Array.IndexOf(grantedScopes, StellaOpsScopes.VexIngest) >= 0;
var hasVexRead = grantedScopes.Length > 0 && Array.IndexOf(grantedScopes, StellaOpsScopes.VexRead) >= 0;
var hasVulnRead = grantedScopes.Length > 0 && Array.IndexOf(grantedScopes, StellaOpsScopes.VulnRead) >= 0;
var tenantScopeForAudit = hasGraphWrite
? StellaOpsScopes.GraphWrite
@@ -302,6 +307,38 @@ internal sealed class ValidateClientCredentialsHandler : IOpenIddictServerHandle
return;
}
if ((hasAdvisoryIngest || hasAdvisoryRead) && !EnsureTenantAssigned())
{
var advisoryScope = hasAdvisoryIngest ? StellaOpsScopes.AdvisoryIngest : StellaOpsScopes.AdvisoryRead;
context.Transaction.Properties[AuthorityOpenIddictConstants.AuditInvalidScopeProperty] = advisoryScope;
context.Reject(OpenIddictConstants.Errors.InvalidClient, "Advisory scopes require a tenant assignment.");
logger.LogWarning(
"Client credentials validation failed for {ClientId}: advisory scopes require tenant assignment.",
document.ClientId);
return;
}
if ((hasVexIngest || hasVexRead) && !EnsureTenantAssigned())
{
var vexScope = hasVexIngest ? StellaOpsScopes.VexIngest : StellaOpsScopes.VexRead;
context.Transaction.Properties[AuthorityOpenIddictConstants.AuditInvalidScopeProperty] = vexScope;
context.Reject(OpenIddictConstants.Errors.InvalidClient, "VEX scopes require a tenant assignment.");
logger.LogWarning(
"Client credentials validation failed for {ClientId}: vex scopes require tenant assignment.",
document.ClientId);
return;
}
if (hasVulnRead && !EnsureTenantAssigned())
{
context.Transaction.Properties[AuthorityOpenIddictConstants.AuditInvalidScopeProperty] = StellaOpsScopes.VulnRead;
context.Reject(OpenIddictConstants.Errors.InvalidClient, "Vuln Explorer scopes require a tenant assignment.");
logger.LogWarning(
"Client credentials validation failed for {ClientId}: vuln scopes require tenant assignment.",
document.ClientId);
return;
}
if (grantedScopes.Length > 0 &&
Array.IndexOf(grantedScopes, StellaOpsScopes.EffectiveWrite) >= 0)
{

View File

@@ -69,6 +69,12 @@ internal sealed class ValidateAccessTokenHandler : IOpenIddictServerHandler<Open
return;
}
static string? NormalizeTenant(string? value)
=> string.IsNullOrWhiteSpace(value) ? null : value.Trim().ToLowerInvariant();
var identity = context.Principal.Identity as ClaimsIdentity;
var principalTenant = NormalizeTenant(context.Principal.GetClaim(StellaOpsClaimTypes.Tenant));
using var activity = activitySource.StartActivity("authority.token.validate_access", ActivityKind.Internal);
activity?.SetTag("authority.endpoint", context.EndpointType switch
{
@@ -111,21 +117,43 @@ internal sealed class ValidateAccessTokenHandler : IOpenIddictServerHandler<Open
if (tokenDocument is not null)
{
EnsureSenderConstraintClaims(context.Principal, tokenDocument);
var documentTenant = NormalizeTenant(tokenDocument.Tenant);
if (documentTenant is not null)
{
if (principalTenant is null)
{
if (identity is not null)
{
identity.SetClaim(StellaOpsClaimTypes.Tenant, documentTenant);
principalTenant = documentTenant;
}
}
else if (!string.Equals(principalTenant, documentTenant, StringComparison.Ordinal))
{
context.Reject(OpenIddictConstants.Errors.InvalidToken, "The token tenant does not match the issued tenant.");
logger.LogWarning(
"Access token validation failed: tenant mismatch for token {TokenId}. PrincipalTenant={PrincipalTenant}; DocumentTenant={DocumentTenant}.",
tokenDocument.TokenId,
principalTenant,
documentTenant);
return;
}
metadataAccessor.SetTenant(documentTenant);
}
}
if (!context.IsRejected && tokenDocument is not null)
{
await TrackTokenUsageAsync(context, tokenDocument, context.Principal, session).ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(tokenDocument.Tenant))
{
metadataAccessor.SetTenant(tokenDocument.Tenant);
}
}
var clientId = context.Principal.GetClaim(OpenIddictConstants.Claims.ClientId);
AuthorityClientDocument? clientDocument = null;
if (!string.IsNullOrWhiteSpace(clientId))
{
var clientDocument = await clientStore.FindByClientIdAsync(clientId, context.CancellationToken, session).ConfigureAwait(false);
clientDocument = await clientStore.FindByClientIdAsync(clientId, context.CancellationToken, session).ConfigureAwait(false);
if (clientDocument is null || clientDocument.Disabled)
{
context.Reject(OpenIddictConstants.Errors.InvalidClient, "The client associated with the token is not permitted.");
@@ -134,15 +162,43 @@ internal sealed class ValidateAccessTokenHandler : IOpenIddictServerHandler<Open
}
}
if (context.Principal.Identity is not ClaimsIdentity identity)
if (clientDocument is not null &&
clientDocument.Properties.TryGetValue(AuthorityClientMetadataKeys.Tenant, out var clientTenantRaw))
{
var clientTenant = NormalizeTenant(clientTenantRaw);
if (clientTenant is not null)
{
if (principalTenant is null)
{
if (identity is not null)
{
identity.SetClaim(StellaOpsClaimTypes.Tenant, clientTenant);
principalTenant = clientTenant;
}
}
else if (!string.Equals(principalTenant, clientTenant, StringComparison.Ordinal))
{
context.Reject(OpenIddictConstants.Errors.InvalidToken, "The token tenant does not match the registered client tenant.");
logger.LogWarning(
"Access token validation failed: tenant mismatch for client {ClientId}. PrincipalTenant={PrincipalTenant}; ClientTenant={ClientTenant}.",
clientId,
principalTenant,
clientTenant);
return;
}
metadataAccessor.SetTenant(clientTenant);
}
}
if (identity is null)
{
return;
}
var tenantClaim = context.Principal.GetClaim(StellaOpsClaimTypes.Tenant);
if (!string.IsNullOrWhiteSpace(tenantClaim))
if (principalTenant is not null)
{
metadataAccessor.SetTenant(tenantClaim);
metadataAccessor.SetTenant(principalTenant);
}
var providerName = context.Principal.GetClaim(StellaOpsClaimTypes.IdentityProvider);

View File

@@ -2,10 +2,13 @@
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|----|--------|----------|------------|-------------|---------------|
| AUTH-AOC-19-001 | DONE (2025-10-26) | Authority Core & Security Guild | — | Introduce scopes `advisory:read`, `advisory:ingest`, `vex:read`, `vex:ingest`, `aoc:verify` with configuration binding, migrations, and offline kit defaults. | Scopes published in metadata/OpenAPI, configuration validates scope lists, tests cover token issuance + enforcement. |
| AUTH-AOC-19-002 | DOING (2025-10-26) | Authority Core & Security Guild | AUTH-AOC-19-001 | Propagate tenant claim + scope enforcement for ingestion identities; ensure cross-tenant writes/read blocked and audit logs capture tenant context. | Tenant claim injected into downstream services; forbidden cross-tenant access rejected; audit/log fixtures updated. |
| AUTH-AOC-19-002 | DONE (2025-10-27) | Authority Core & Security Guild | AUTH-AOC-19-001 | Propagate tenant claim + scope enforcement for ingestion identities; ensure cross-tenant writes/read blocked and audit logs capture tenant context. | Tenant claim injected into downstream services; forbidden cross-tenant access rejected; audit/log fixtures updated. |
> 2025-10-26: Rate limiter metadata/audit records now include tenants, password grant scopes/tenants enforced, token persistence + tests updated. Docs refresh tracked via AUTH-AOC-19-003.
| AUTH-AOC-19-003 | TODO | Authority Core & Docs Guild | AUTH-AOC-19-001 | Update Authority docs and sample configs to describe new scopes, tenancy enforcement, and verify endpoints. | Docs and examples refreshed; release notes prepared; smoke tests confirm new scopes required. |
> 2025-10-27: Client credential ingestion scopes now require tenant assignment; access token validation backfills tenants and rejects cross-tenant mismatches with tests.
> 2025-10-27: `dotnet test` blocked — Concelier build fails (`AdvisoryObservationQueryService` returns `ImmutableHashSet<string?>`), preventing Authority test suite run; waiting on Concelier fix before rerun.
| AUTH-AOC-19-003 | DONE (2025-10-27) | Authority Core & Docs Guild | AUTH-AOC-19-001 | Update Authority docs and sample configs to describe new scopes, tenancy enforcement, and verify endpoints. | Docs and examples refreshed; release notes prepared; smoke tests confirm new scopes required. |
> 2025-10-26: Docs updated (`docs/11_AUTHORITY.md`, Concelier audit runbook, `docs/security/authority-scopes.md`); sample config highlights tenant-aware clients. Release notes + smoke verification pending (blocked on Concelier/Excititor smoke updates).
> 2025-10-27: Scope catalogue aligned with `advisory:ingest/advisory:read/vex:ingest/vex:read`, `aoc:verify` pairing documented, console/CLI references refreshed, and `etc/authority.yaml.sample` updated to require read scopes for verification clients.
## Policy Engine v2
@@ -38,6 +41,7 @@
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|----|--------|----------|------------|-------------|---------------|
| AUTH-VULN-24-001 | TODO | Authority Core & Security Guild | AUTH-GRAPH-21-001 | Extend scopes to include `vuln:read` and signed permalinks with scoped claims for Vuln Explorer; update metadata. | Scopes published; permalinks validated; integration tests cover RBAC. |
> 2025-10-27: Paused work after exploratory spike (scope enforcement still outstanding); no functional changes merged.
## Orchestrator Dashboard
@@ -54,6 +58,8 @@
| AUTH-CONSOLE-23-001 | TODO | Authority Core & Security Guild | AUTH-POLICY-20-001 | Register StellaOps Console confidential client with OIDC PKCE support, short-lived ID/access tokens, `console:*` audience claims, and SPA-friendly refresh (token exchange endpoint). Publish discovery metadata + offline kit defaults. | Client registration committed, configuration templates updated, integration tests validate PKCE + scope issuance, security review recorded. |
| AUTH-CONSOLE-23-002 | TODO | Authority Core & Security Guild | AUTH-CONSOLE-23-001, AUTH-AOC-19-002 | Expose tenant catalog, user profile, and token introspection endpoints required by Console (fresh-auth prompts, scope checks); enforce tenant header requirements and audit logging with correlation IDs. | Endpoints ship with RBAC enforcement, audit logs include tenant+scope, integration tests cover unauthorized/tenant-mismatch scenarios. |
| AUTH-CONSOLE-23-003 | TODO | Authority Core & Docs Guild | AUTH-CONSOLE-23-001, AUTH-CONSOLE-23-002 | Update security docs/config samples for Console flows (PKCE, tenant badge, fresh-auth for admin actions, session inactivity timeouts) with compliance checklist. | Docs merged, config samples validated, release notes updated, ops runbook references new flows. |
> 2025-10-28: `docs/security/console-security.md` drafted with PKCE + DPoP (120s OpTok, 300s fresh-auth) and scope table. Authority Core to confirm `/fresh-auth` semantics, token lifetimes, and scope bundles align before closing task.
| AUTH-CONSOLE-23-004 | TODO | Authority Core & Security Guild | AUTH-CONSOLE-23-003, DOCS-CONSOLE-23-012 | Validate console security guide assumptions (120s OpTok TTL, 300s fresh-auth window, scope bundles) against Authority implementation and update configs/audit fixtures if needed. | Confirmation recorded in sprint log; Authority config samples/tests updated when adjustments required; `/fresh-auth` behaviour documented in release notes. |
## Policy Studio (Sprint 27)
@@ -61,6 +67,7 @@
|----|--------|----------|------------|-------------|---------------|
| AUTH-POLICY-27-001 | TODO | Authority Core & Security Guild | AUTH-POLICY-20-001, AUTH-CONSOLE-23-001 | Define Policy Studio roles (`policy:author`, `policy:review`, `policy:approve`, `policy:operate`, `policy:audit`) with tenant-scoped claims, update issuer metadata, and seed offline kit defaults. | Scopes/roles exposed via discovery docs; tokens issued with correct claims; integration tests cover role combinations; docs updated. |
| AUTH-POLICY-27-002 | TODO | Authority Core & Security Guild | AUTH-POLICY-27-001, REGISTRY-API-27-007 | Provide attestation signing service bindings (OIDC token exchange, cosign integration) and enforce publish/promote scope checks, fresh-auth requirements, and audit logging. | Publish/promote requests require fresh auth + correct scopes; attestations signed with validated identity; audit logs enriched with digest + tenant; integration tests pass. |
> Docs dependency: `DOCS-POLICY-27-009` awaiting signing guidance from this work.
| AUTH-POLICY-27-003 | TODO | Authority Core & Docs Guild | AUTH-POLICY-27-001, AUTH-POLICY-27-002 | Update Authority configuration/docs for Policy Studio roles, signing policies, approval workflows, and CLI integration; include compliance checklist. | Docs merged; samples validated; governance checklist appended; release notes updated. |
## Exceptions v1