up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

This commit is contained in:
master
2025-11-03 19:12:51 +02:00
parent b1e78fe412
commit f72c5c513a
17 changed files with 931 additions and 198 deletions

View File

@@ -110,6 +110,24 @@ public sealed class ServiceAccountAdminEndpointsTests : IClassFixture<AuthorityW
{
var options = scope.ServiceProvider.GetRequiredService<IOptions<StellaOpsAuthorityOptions>>();
Assert.True(options.Value.Bootstrap.Enabled);
var seededAccount = Assert.Single(options.Value.Delegation.ServiceAccounts);
Assert.True(seededAccount.Enabled);
var accountStore = scope.ServiceProvider.GetRequiredService<IAuthorityServiceAccountStore>();
var existingDocument = await accountStore.FindByAccountIdAsync(ServiceAccountId, CancellationToken.None);
var document = existingDocument ?? new AuthorityServiceAccountDocument { AccountId = ServiceAccountId };
document.Tenant = TenantId;
document.DisplayName = "Observability Exporter";
document.Description = "Automates evidence exports.";
document.Enabled = true;
document.AllowedScopes = new List<string> { "jobs:read", "findings:read" };
document.AuthorizedClients = new List<string> { "export-center-worker" };
document.Attributes = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase)
{
["env"] = new List<string> { "prod" },
["owner"] = new List<string> { "vuln-team" },
["business_tier"] = new List<string> { "tier-1" }
};
await accountStore.UpsertAsync(document, CancellationToken.None);
var endpoints = scope.ServiceProvider.GetRequiredService<EndpointDataSource>().Endpoints;
var serviceAccountsEndpoint = endpoints
.OfType<RouteEndpoint>()
@@ -142,6 +160,14 @@ public sealed class ServiceAccountAdminEndpointsTests : IClassFixture<AuthorityW
Assert.Equal(new[] { "vuln-team" }, ownerValues);
Assert.True(serviceAccount.Attributes.TryGetValue("business_tier", out var tierValues));
Assert.Equal(new[] { "tier-1" }, tierValues);
await using (var verificationScope = app.Services.CreateAsyncScope())
{
var accountStore = verificationScope.ServiceProvider.GetRequiredService<IAuthorityServiceAccountStore>();
var document = await accountStore.FindByAccountIdAsync(ServiceAccountId, CancellationToken.None);
Assert.NotNull(document);
Assert.True(document!.Enabled);
}
}
[Fact]
@@ -165,7 +191,7 @@ public sealed class ServiceAccountAdminEndpointsTests : IClassFixture<AuthorityW
TokenKind = "service_account"
};
await tokenStore.InsertAsync(document, CancellationToken.None).ConfigureAwait(false);
await tokenStore.InsertAsync(document, CancellationToken.None);
}
using var client = app.CreateClient();
@@ -251,7 +277,7 @@ public sealed class ServiceAccountAdminEndpointsTests : IClassFixture<AuthorityW
Tenant = TenantId,
ServiceAccountId = ServiceAccountId,
TokenKind = "service_account"
}, CancellationToken.None).ConfigureAwait(false);
}, CancellationToken.None);
}
}
@@ -278,14 +304,14 @@ public sealed class ServiceAccountAdminEndpointsTests : IClassFixture<AuthorityW
foreach (var tokenId in tokenIds)
{
var sessionAccessor = scope.ServiceProvider.GetRequiredService<IAuthorityMongoSessionAccessor>();
var session = await sessionAccessor.GetSessionAsync().ConfigureAwait(false);
var token = await tokenStore.FindByTokenIdAsync(tokenId, CancellationToken.None, session).ConfigureAwait(false);
var session = await sessionAccessor.GetSessionAsync();
var token = await tokenStore.FindByTokenIdAsync(tokenId, CancellationToken.None, session);
Assert.NotNull(token);
Assert.Equal("revoked", token!.Status);
}
}
var audit = Assert.Single(sink.Events.Where(evt => evt.EventType == "authority.delegation.revoked"));
var audit = Assert.Single(sink.Events, evt => evt.EventType == "authority.delegation.revoked");
Assert.Equal(AuthEventOutcome.Success, audit.Outcome);
Assert.Equal("operator_request", audit.Reason);
Assert.Contains(audit.Properties, property =>
@@ -389,7 +415,7 @@ public sealed class ServiceAccountAdminEndpointsTests : IClassFixture<AuthorityW
Tenant = TenantId,
ServiceAccountId = ServiceAccountId,
TokenKind = "service_account"
}, CancellationToken.None).ConfigureAwait(false);
}, CancellationToken.None);
}
using var client = app.CreateClient();
@@ -447,7 +473,7 @@ public sealed class ServiceAccountAdminEndpointsTests : IClassFixture<AuthorityW
Tenant = TenantId,
ServiceAccountId = ServiceAccountId,
TokenKind = "service_account"
}, CancellationToken.None).ConfigureAwait(false);
}, CancellationToken.None);
await tokenStore.InsertAsync(new AuthorityTokenDocument
{
@@ -459,7 +485,7 @@ public sealed class ServiceAccountAdminEndpointsTests : IClassFixture<AuthorityW
Tenant = TenantId,
ServiceAccountId = ServiceAccountId,
TokenKind = "service_account"
}, CancellationToken.None).ConfigureAwait(false);
}, CancellationToken.None);
}
using var client = app.CreateClient();
@@ -491,7 +517,7 @@ public sealed class ServiceAccountAdminEndpointsTests : IClassFixture<AuthorityW
{
await using var scope = firstApp.Services.CreateAsyncScope();
var store = scope.ServiceProvider.GetRequiredService<IAuthorityServiceAccountStore>();
var document = await store.FindByAccountIdAsync(ServiceAccountId, CancellationToken.None).ConfigureAwait(false);
var document = await store.FindByAccountIdAsync(ServiceAccountId, CancellationToken.None);
Assert.NotNull(document);
initialId = document!.Id;
@@ -502,7 +528,7 @@ public sealed class ServiceAccountAdminEndpointsTests : IClassFixture<AuthorityW
{
await using var scope = secondApp.Services.CreateAsyncScope();
var store = scope.ServiceProvider.GetRequiredService<IAuthorityServiceAccountStore>();
var document = await store.FindByAccountIdAsync(ServiceAccountId, CancellationToken.None).ConfigureAwait(false);
var document = await store.FindByAccountIdAsync(ServiceAccountId, CancellationToken.None);
Assert.NotNull(document);
Assert.Equal(initialId, document!.Id);
@@ -521,6 +547,7 @@ public sealed class ServiceAccountAdminEndpointsTests : IClassFixture<AuthorityW
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__QUOTAS__MAXACTIVETOKENS", "50");
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__ACCOUNTID", ServiceAccountId);
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__TENANT", TenantId);
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__ENABLED", "true");
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__DISPLAYNAME", "Observability Exporter");
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__DESCRIPTION", "Automates evidence exports.");
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__ALLOWEDSCOPES__0", "jobs:read");
@@ -568,25 +595,31 @@ public sealed class ServiceAccountAdminEndpointsTests : IClassFixture<AuthorityW
options.Delegation.Quotas.MaxActiveTokens = 50;
if (options.Delegation.ServiceAccounts.Count == 0)
var serviceAccount = options.Delegation.ServiceAccounts
.FirstOrDefault(account => string.Equals(account.AccountId, ServiceAccountId, StringComparison.OrdinalIgnoreCase));
if (serviceAccount is null)
{
var serviceAccount = new AuthorityServiceAccountSeedOptions
{
AccountId = ServiceAccountId,
Tenant = TenantId,
DisplayName = "Observability Exporter",
Description = "Automates evidence exports."
};
serviceAccount.AllowedScopes.Add("jobs:read");
serviceAccount.AllowedScopes.Add("findings:read");
serviceAccount.AuthorizedClients.Add("export-center-worker");
serviceAccount.Attributes["env"] = new List<string> { "prod" };
serviceAccount.Attributes["owner"] = new List<string> { "vuln-team" };
serviceAccount.Attributes["business_tier"] = new List<string> { "tier-1" };
serviceAccount = new AuthorityServiceAccountSeedOptions();
options.Delegation.ServiceAccounts.Add(serviceAccount);
}
serviceAccount.AccountId = ServiceAccountId;
serviceAccount.Tenant = TenantId;
serviceAccount.DisplayName = "Observability Exporter";
serviceAccount.Description = "Automates evidence exports.";
serviceAccount.Enabled = true;
serviceAccount.AllowedScopes.Clear();
serviceAccount.AllowedScopes.Add("jobs:read");
serviceAccount.AllowedScopes.Add("findings:read");
serviceAccount.AuthorizedClients.Clear();
serviceAccount.AuthorizedClients.Add("export-center-worker");
serviceAccount.Attributes["env"] = new List<string> { "prod" };
serviceAccount.Attributes["owner"] = new List<string> { "vuln-team" };
serviceAccount.Attributes["business_tier"] = new List<string> { "tier-1" };
});
});

View File

@@ -52,11 +52,11 @@ public sealed class ConsoleEndpointsTests
Assert.Equal(1, tenants.GetArrayLength());
Assert.Equal("tenant-default", tenants[0].GetProperty("id").GetString());
var events = sink.Events;
var authorizeEvent = Assert.Single(events.Where(evt => evt.EventType == "authority.resource.authorize"));
Assert.Equal(AuthEventOutcome.Success, authorizeEvent.Outcome);
var consoleEvent = Assert.Single(events.Where(evt => evt.EventType == "authority.console.tenants.read"));
var events = sink.Events;
var authorizeEvent = Assert.Single(events, evt => evt.EventType == "authority.resource.authorize");
Assert.Equal(AuthEventOutcome.Success, authorizeEvent.Outcome);
var consoleEvent = Assert.Single(events, evt => evt.EventType == "authority.console.tenants.read");
Assert.Equal(AuthEventOutcome.Success, consoleEvent.Outcome);
Assert.Contains("tenant.resolved", consoleEvent.Properties.Select(property => property.Name));
Assert.Equal(2, events.Count);
@@ -146,10 +146,10 @@ public sealed class ConsoleEndpointsTests
Assert.Equal("tenant-default", json.RootElement.GetProperty("tenant").GetString());
var events = sink.Events;
var authorizeEvent = Assert.Single(events.Where(evt => evt.EventType == "authority.resource.authorize"));
var authorizeEvent = Assert.Single(events, evt => evt.EventType == "authority.resource.authorize");
Assert.Equal(AuthEventOutcome.Success, authorizeEvent.Outcome);
var consoleEvent = Assert.Single(events.Where(evt => evt.EventType == "authority.console.profile.read"));
var consoleEvent = Assert.Single(events, evt => evt.EventType == "authority.console.profile.read");
Assert.Equal(AuthEventOutcome.Success, consoleEvent.Outcome);
Assert.Equal(2, events.Count);
}
@@ -184,10 +184,10 @@ public sealed class ConsoleEndpointsTests
Assert.Equal("token-abc", json.RootElement.GetProperty("tokenId").GetString());
var events = sink.Events;
var authorizeEvent = Assert.Single(events.Where(evt => evt.EventType == "authority.resource.authorize"));
var authorizeEvent = Assert.Single(events, evt => evt.EventType == "authority.resource.authorize");
Assert.Equal(AuthEventOutcome.Success, authorizeEvent.Outcome);
var consoleEvent = Assert.Single(events.Where(evt => evt.EventType == "authority.console.token.introspect"));
var consoleEvent = Assert.Single(events, evt => evt.EventType == "authority.console.token.introspect");
Assert.Equal(AuthEventOutcome.Success, consoleEvent.Outcome);
Assert.Equal(2, events.Count);
}
@@ -287,7 +287,7 @@ public sealed class ConsoleEndpointsTests
app.UseAuthorization();
app.MapConsoleEndpoints();
await app.StartAsync().ConfigureAwait(false);
await app.StartAsync();
return app;
}
@@ -321,12 +321,11 @@ public sealed class ConsoleEndpointsTests
private sealed class TestAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public TestAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock)
: base(options, logger, encoder, clock)
public TestAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder)
: base(options, logger, encoder)
{
}
@@ -356,4 +355,4 @@ internal static class HostTestClientExtensions
internal static class TestAuthenticationDefaults
{
public const string AuthenticationScheme = "AuthorityConsoleTests";
}
}

View File

@@ -31,6 +31,7 @@ public sealed class AuthorityWebApplicationFactory : WebApplicationFactory<Progr
private const string DelegationQuotaKey = "STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__QUOTAS__MAXACTIVETOKENS";
private const string ServiceAccountIdKey = "STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__ACCOUNTID";
private const string ServiceAccountTenantKey = "STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__TENANT";
private const string ServiceAccountEnabledKey = "STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__ENABLED";
private const string ServiceAccountDisplayNameKey = "STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__DISPLAYNAME";
private const string ServiceAccountDescriptionKey = "STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__DESCRIPTION";
private const string ServiceAccountScope0Key = "STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__ALLOWEDSCOPES__0";
@@ -64,6 +65,7 @@ public sealed class AuthorityWebApplicationFactory : WebApplicationFactory<Progr
Environment.SetEnvironmentVariable(DelegationQuotaKey, "50");
Environment.SetEnvironmentVariable(ServiceAccountIdKey, "svc-observer");
Environment.SetEnvironmentVariable(ServiceAccountTenantKey, "tenant-default");
Environment.SetEnvironmentVariable(ServiceAccountEnabledKey, "true");
Environment.SetEnvironmentVariable(ServiceAccountDisplayNameKey, "Observability Exporter");
Environment.SetEnvironmentVariable(ServiceAccountDescriptionKey, "Automates evidence exports.");
Environment.SetEnvironmentVariable(ServiceAccountScope0Key, "jobs:read");
@@ -85,11 +87,12 @@ public sealed class AuthorityWebApplicationFactory : WebApplicationFactory<Progr
["Authority:SchemaVersion"] = "1",
["Authority:Storage:ConnectionString"] = mongoRunner.ConnectionString,
["Authority:Storage:DatabaseName"] = "authority-tests",
["Authority:Signing:Enabled"] = "false",
["Authority:Notifications:AckTokens:Enabled"] = "false",
["Authority:Notifications:Webhooks:Enabled"] = "false"
});
});
["Authority:Signing:Enabled"] = "false",
["Authority:Notifications:AckTokens:Enabled"] = "false",
["Authority:Notifications:Webhooks:Enabled"] = "false",
["Authority:Delegation:ServiceAccounts:0:Enabled"] = "true"
});
});
}
@@ -103,11 +106,12 @@ public sealed class AuthorityWebApplicationFactory : WebApplicationFactory<Progr
["Authority:SchemaVersion"] = "1",
["Authority:Storage:ConnectionString"] = mongoRunner.ConnectionString,
["Authority:Storage:DatabaseName"] = "authority-tests",
["Authority:Signing:Enabled"] = "false",
["Authority:Notifications:AckTokens:Enabled"] = "false",
["Authority:Notifications:Webhooks:Enabled"] = "false"
});
});
["Authority:Signing:Enabled"] = "false",
["Authority:Notifications:AckTokens:Enabled"] = "false",
["Authority:Notifications:Webhooks:Enabled"] = "false",
["Authority:Delegation:ServiceAccounts:0:Enabled"] = "true"
});
});
return base.CreateHost(builder);
}
@@ -150,6 +154,7 @@ public sealed class AuthorityWebApplicationFactory : WebApplicationFactory<Progr
Environment.SetEnvironmentVariable(DelegationQuotaKey, null);
Environment.SetEnvironmentVariable(ServiceAccountIdKey, null);
Environment.SetEnvironmentVariable(ServiceAccountTenantKey, null);
Environment.SetEnvironmentVariable(ServiceAccountEnabledKey, null);
Environment.SetEnvironmentVariable(ServiceAccountDisplayNameKey, null);
Environment.SetEnvironmentVariable(ServiceAccountDescriptionKey, null);
Environment.SetEnvironmentVariable(ServiceAccountScope0Key, null);
@@ -168,7 +173,7 @@ public sealed class AuthorityWebApplicationFactory : WebApplicationFactory<Progr
// ignore cleanup failures
}
await base.DisposeAsync().ConfigureAwait(false);
await base.DisposeAsync();
}
Task IAsyncLifetime.DisposeAsync() => DisposeAsync().AsTask();

View File

@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
namespace StellaOps.Authority.Tests.Infrastructure;
internal sealed class EnvironmentVariableScope : IDisposable
{
private readonly Dictionary<string, string?> originals = new(StringComparer.Ordinal);
private bool disposed;
public EnvironmentVariableScope(IEnumerable<KeyValuePair<string, string?>> overrides)
{
if (overrides is null)
{
throw new ArgumentNullException(nameof(overrides));
}
foreach (var kvp in overrides)
{
if (originals.ContainsKey(kvp.Key))
{
continue;
}
originals.Add(kvp.Key, Environment.GetEnvironmentVariable(kvp.Key));
Environment.SetEnvironmentVariable(kvp.Key, kvp.Value);
}
}
public void Dispose()
{
if (disposed)
{
return;
}
foreach (var kvp in originals)
{
Environment.SetEnvironmentVariable(kvp.Key, kvp.Value);
}
disposed = true;
}
}

View File

@@ -17,9 +17,8 @@ internal sealed class TestAuthHandler : AuthenticationHandler<AuthenticationSche
public TestAuthHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock)
: base(options, logger, encoder, clock)
UrlEncoder encoder)
: base(options, logger, encoder)
{
}

View File

@@ -35,6 +35,14 @@ public sealed class NotifyAckTokenRotationEndpointTests : IClassFixture<Authorit
[Fact]
public async Task Rotate_ReturnsOk_AndEmitsAuditEvent()
{
const string AckEnabledKey = "STELLAOPS_AUTHORITY_AUTHORITY__NOTIFICATIONS__ACKTOKENS__ENABLED";
const string AckActiveKeyIdKey = "STELLAOPS_AUTHORITY_AUTHORITY__NOTIFICATIONS__ACKTOKENS__ACTIVEKEYID";
const string AckKeyPathKey = "STELLAOPS_AUTHORITY_AUTHORITY__NOTIFICATIONS__ACKTOKENS__KEYPATH";
const string AckKeySourceKey = "STELLAOPS_AUTHORITY_AUTHORITY__NOTIFICATIONS__ACKTOKENS__KEYSOURCE";
const string AckAlgorithmKey = "STELLAOPS_AUTHORITY_AUTHORITY__NOTIFICATIONS__ACKTOKENS__ALGORITHM";
const string WebhooksEnabledKey = "STELLAOPS_AUTHORITY_AUTHORITY__NOTIFICATIONS__WEBHOOKS__ENABLED";
const string WebhooksAllowedHost0Key = "STELLAOPS_AUTHORITY_AUTHORITY__NOTIFICATIONS__WEBHOOKS__ALLOWEDHOSTS__0";
var tempDir = Directory.CreateTempSubdirectory("ack-rotation-success");
try
{
@@ -43,10 +51,21 @@ public sealed class NotifyAckTokenRotationEndpointTests : IClassFixture<Authorit
CreateEcPrivateKey(key1Path);
CreateEcPrivateKey(key2Path);
using var env = new EnvironmentVariableScope(new[]
{
new KeyValuePair<string, string?>(AckEnabledKey, "true"),
new KeyValuePair<string, string?>(AckActiveKeyIdKey, "ack-key-1"),
new KeyValuePair<string, string?>(AckKeyPathKey, key1Path),
new KeyValuePair<string, string?>(AckKeySourceKey, "file"),
new KeyValuePair<string, string?>(AckAlgorithmKey, SignatureAlgorithms.Es256),
new KeyValuePair<string, string?>(WebhooksEnabledKey, "true"),
new KeyValuePair<string, string?>(WebhooksAllowedHost0Key, "hooks.slack.com")
});
var sink = new RecordingAuthEventSink();
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T12:00:00Z"));
using var app = factory.WithWebHostBuilder(host =>
using var scopedFactory = factory.WithWebHostBuilder(host =>
{
host.ConfigureAppConfiguration((_, configuration) =>
{
@@ -87,7 +106,8 @@ public sealed class NotifyAckTokenRotationEndpointTests : IClassFixture<Authorit
});
});
using var client = app.CreateClient();
using var client = scopedFactory.CreateClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(TestAuthHandler.SchemeName);
client.DefaultRequestHeaders.Add("X-Test-Scopes", StellaOpsScopes.NotifyAdmin);
client.DefaultRequestHeaders.Add("X-Test-Tenant", "tenant-default");
@@ -106,7 +126,7 @@ public sealed class NotifyAckTokenRotationEndpointTests : IClassFixture<Authorit
Assert.Equal("ack-key-2", payload!.ActiveKeyId);
Assert.Equal("ack-key-1", payload.PreviousKeyId);
var rotationEvent = Assert.Single(sink.Events.Where(evt => evt.EventType == "notify.ack.key_rotated"));
var rotationEvent = Assert.Single(sink.Events, evt => evt.EventType == "notify.ack.key_rotated");
Assert.Equal(AuthEventOutcome.Success, rotationEvent.Outcome);
Assert.Contains(rotationEvent.Properties, property =>
string.Equals(property.Name, "notify.ack.key_id", StringComparison.Ordinal) &&
@@ -184,7 +204,7 @@ public sealed class NotifyAckTokenRotationEndpointTests : IClassFixture<Authorit
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var failureEvent = Assert.Single(sink.Events.Where(evt => evt.EventType == "notify.ack.key_rotation_failed"));
var failureEvent = Assert.Single(sink.Events, evt => evt.EventType == "notify.ack.key_rotation_failed");
Assert.Equal(AuthEventOutcome.Failure, failureEvent.Outcome);
Assert.Contains("keyId", failureEvent.Reason, StringComparison.OrdinalIgnoreCase);
}

View File

@@ -27,7 +27,7 @@ public sealed class OpenApiDiscoveryEndpointTests : IClassFixture<AuthorityWebAp
{
using var client = factory.CreateClient();
using var response = await client.GetAsync("/.well-known/openapi").ConfigureAwait(false);
using var response = await client.GetAsync("/.well-known/openapi");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Headers.ETag);
@@ -36,7 +36,7 @@ public sealed class OpenApiDiscoveryEndpointTests : IClassFixture<AuthorityWebAp
var contentType = response.Content.Headers.ContentType?.ToString();
Assert.Equal("application/openapi+json; charset=utf-8", contentType);
var payload = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
var payload = await response.Content.ReadAsStringAsync();
using var document = JsonDocument.Parse(payload);
Assert.Equal("3.1.0", document.RootElement.GetProperty("openapi").GetString());
@@ -61,12 +61,12 @@ public sealed class OpenApiDiscoveryEndpointTests : IClassFixture<AuthorityWebAp
using var request = new HttpRequestMessage(HttpMethod.Get, "/.well-known/openapi");
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/openapi+yaml"));
using var response = await client.SendAsync(request).ConfigureAwait(false);
using var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("application/openapi+yaml; charset=utf-8", response.Content.Headers.ContentType?.ToString());
var payload = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
var payload = await response.Content.ReadAsStringAsync();
Assert.StartsWith("openapi: 3.1.0", payload.TrimStart(), StringComparison.Ordinal);
}
@@ -75,14 +75,14 @@ public sealed class OpenApiDiscoveryEndpointTests : IClassFixture<AuthorityWebAp
{
using var client = factory.CreateClient();
using var initialResponse = await client.GetAsync("/.well-known/openapi").ConfigureAwait(false);
using var initialResponse = await client.GetAsync("/.well-known/openapi");
var etag = initialResponse.Headers.ETag;
Assert.NotNull(etag);
using var conditionalRequest = new HttpRequestMessage(HttpMethod.Get, "/.well-known/openapi");
conditionalRequest.Headers.IfNoneMatch.Add(etag!);
using var conditionalResponse = await client.SendAsync(conditionalRequest).ConfigureAwait(false);
using var conditionalResponse = await client.SendAsync(conditionalRequest);
Assert.Equal(HttpStatusCode.NotModified, conditionalResponse.StatusCode);
Assert.Equal(etag!.Tag, conditionalResponse.Headers.ETag?.Tag);

View File

@@ -17,9 +17,9 @@ using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Microsoft.IdentityModel.Tokens;
using StellaOps.Configuration;
using OpenIddict.Abstractions;
using StellaOps.Authority.Security;
using StellaOps.Auth.Security.Dpop;
using OpenIddict.Abstractions;
using OpenIddict.Extensions;
using OpenIddict.Server;
using OpenIddict.Server.AspNetCore;
@@ -225,7 +225,7 @@ public class ClientCredentialsHandlersTests
NullLogger<ValidateClientCredentialsHandler>.Instance);
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "jobs:read");
transaction.Request.SetParameter(AuthorityOpenIddictConstants.ServiceAccountParameterName, "svc-observer");
SetParameter(transaction, AuthorityOpenIddictConstants.ServiceAccountParameterName, "svc-observer");
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await handler.HandleAsync(context);
@@ -296,7 +296,7 @@ public class ClientCredentialsHandlersTests
NullLogger<ValidateClientCredentialsHandler>.Instance);
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "jobs:read");
transaction.Request.SetParameter(AuthorityOpenIddictConstants.ServiceAccountParameterName, "svc-observer");
SetParameter(transaction, AuthorityOpenIddictConstants.ServiceAccountParameterName, "svc-observer");
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await handler.HandleAsync(context);
@@ -346,7 +346,7 @@ public class ClientCredentialsHandlersTests
NullLogger<ValidateClientCredentialsHandler>.Instance);
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "vuln:view");
transaction.Request.SetParameter(AuthorityOpenIddictConstants.ServiceAccountParameterName, "svc-vuln");
SetParameter(transaction, AuthorityOpenIddictConstants.ServiceAccountParameterName, "svc-vuln");
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await handler.HandleAsync(context);
@@ -398,10 +398,10 @@ public class ClientCredentialsHandlersTests
NullLogger<ValidateClientCredentialsHandler>.Instance);
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "vuln:view");
transaction.Request.SetParameter(AuthorityOpenIddictConstants.ServiceAccountParameterName, "svc-vuln");
transaction.Request.SetParameter(AuthorityOpenIddictConstants.VulnEnvironmentParameterName, "prod");
transaction.Request.SetParameter(AuthorityOpenIddictConstants.VulnOwnerParameterName, "security");
transaction.Request.SetParameter(AuthorityOpenIddictConstants.VulnBusinessTierParameterName, "tier-1");
SetParameter(transaction, AuthorityOpenIddictConstants.ServiceAccountParameterName, "svc-vuln");
SetParameter(transaction, AuthorityOpenIddictConstants.VulnEnvironmentParameterName, "prod");
SetParameter(transaction, AuthorityOpenIddictConstants.VulnOwnerParameterName, "security");
SetParameter(transaction, AuthorityOpenIddictConstants.VulnBusinessTierParameterName, "tier-1");
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await handler.HandleAsync(context);
@@ -464,10 +464,10 @@ public class ClientCredentialsHandlersTests
{
AccessTokenLifetime = TimeSpan.FromMinutes(5)
};
transaction.Request.SetParameter(AuthorityOpenIddictConstants.ServiceAccountParameterName, "svc-vuln");
transaction.Request.SetParameter(AuthorityOpenIddictConstants.VulnEnvironmentParameterName, "prod");
transaction.Request.SetParameter(AuthorityOpenIddictConstants.VulnOwnerParameterName, "security");
transaction.Request.SetParameter(AuthorityOpenIddictConstants.VulnBusinessTierParameterName, "tier-1");
SetParameter(transaction, AuthorityOpenIddictConstants.ServiceAccountParameterName, "svc-vuln");
SetParameter(transaction, AuthorityOpenIddictConstants.VulnEnvironmentParameterName, "prod");
SetParameter(transaction, AuthorityOpenIddictConstants.VulnOwnerParameterName, "security");
SetParameter(transaction, AuthorityOpenIddictConstants.VulnBusinessTierParameterName, "tier-1");
var validateContext = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await validateHandler.HandleAsync(validateContext);
@@ -1018,8 +1018,8 @@ public class ClientCredentialsHandlersTests
NullLogger<ValidateClientCredentialsHandler>.Instance);
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:operate");
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.OperatorReasonParameterName, "resume source after maintenance");
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.OperatorTicketParameterName, "INC-2045");
SetParameter(transaction, AuthorityOpenIddictConstants.OperatorReasonParameterName, "resume source after maintenance");
SetParameter(transaction, AuthorityOpenIddictConstants.OperatorTicketParameterName, "INC-2045");
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await handler.HandleAsync(context);
@@ -1056,7 +1056,7 @@ public class ClientCredentialsHandlersTests
NullLogger<ValidateClientCredentialsHandler>.Instance);
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:operate");
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.OperatorTicketParameterName, "INC-2045");
SetParameter(transaction, AuthorityOpenIddictConstants.OperatorTicketParameterName, "INC-2045");
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await handler.HandleAsync(context);
@@ -1093,7 +1093,7 @@ public class ClientCredentialsHandlersTests
NullLogger<ValidateClientCredentialsHandler>.Instance);
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:operate");
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.OperatorReasonParameterName, "resume source after maintenance");
SetParameter(transaction, AuthorityOpenIddictConstants.OperatorReasonParameterName, "resume source after maintenance");
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await handler.HandleAsync(context);
@@ -1129,8 +1129,8 @@ public class ClientCredentialsHandlersTests
NullLogger<ValidateClientCredentialsHandler>.Instance);
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:backfill");
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.BackfillReasonParameterName, "Backfill drift repair");
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.BackfillTicketParameterName, "INC-9981");
SetParameter(transaction, AuthorityOpenIddictConstants.BackfillReasonParameterName, "Backfill drift repair");
SetParameter(transaction, AuthorityOpenIddictConstants.BackfillTicketParameterName, "INC-9981");
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await handler.HandleAsync(context);
@@ -1167,7 +1167,7 @@ public class ClientCredentialsHandlersTests
NullLogger<ValidateClientCredentialsHandler>.Instance);
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:backfill");
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.BackfillTicketParameterName, "INC-9981");
SetParameter(transaction, AuthorityOpenIddictConstants.BackfillTicketParameterName, "INC-9981");
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await handler.HandleAsync(context);
@@ -1204,7 +1204,7 @@ public class ClientCredentialsHandlersTests
NullLogger<ValidateClientCredentialsHandler>.Instance);
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:backfill");
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.BackfillReasonParameterName, "Backfill drift repair");
SetParameter(transaction, AuthorityOpenIddictConstants.BackfillReasonParameterName, "Backfill drift repair");
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await handler.HandleAsync(context);
@@ -1242,8 +1242,8 @@ public class ClientCredentialsHandlersTests
var longReason = new string('a', 257);
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:backfill");
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.BackfillReasonParameterName, longReason);
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.BackfillTicketParameterName, "INC-9981");
SetParameter(transaction, AuthorityOpenIddictConstants.BackfillReasonParameterName, longReason);
SetParameter(transaction, AuthorityOpenIddictConstants.BackfillTicketParameterName, "INC-9981");
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await handler.HandleAsync(context);
@@ -1281,8 +1281,8 @@ public class ClientCredentialsHandlersTests
var longTicket = new string('b', 129);
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:backfill");
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.BackfillReasonParameterName, "Backfill drift repair");
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.BackfillTicketParameterName, longTicket);
SetParameter(transaction, AuthorityOpenIddictConstants.BackfillReasonParameterName, "Backfill drift repair");
SetParameter(transaction, AuthorityOpenIddictConstants.BackfillTicketParameterName, longTicket);
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await handler.HandleAsync(context);
@@ -1319,8 +1319,8 @@ public class ClientCredentialsHandlersTests
NullLogger<ValidateClientCredentialsHandler>.Instance);
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:backfill");
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.BackfillReasonParameterName, "Backfill drift repair");
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.BackfillTicketParameterName, "INC-9981");
SetParameter(transaction, AuthorityOpenIddictConstants.BackfillReasonParameterName, "Backfill drift repair");
SetParameter(transaction, AuthorityOpenIddictConstants.BackfillTicketParameterName, "INC-9981");
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await handler.HandleAsync(context);
@@ -1361,8 +1361,8 @@ public class ClientCredentialsHandlersTests
NullLogger<ValidateClientCredentialsHandler>.Instance);
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:operate");
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.OperatorReasonParameterName, "resume source after maintenance");
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.OperatorTicketParameterName, "INC-2045");
SetParameter(transaction, AuthorityOpenIddictConstants.OperatorReasonParameterName, "resume source after maintenance");
SetParameter(transaction, AuthorityOpenIddictConstants.OperatorTicketParameterName, "INC-2045");
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await handler.HandleAsync(context);
@@ -1406,8 +1406,8 @@ public class ClientCredentialsHandlersTests
NullLogger<ValidateClientCredentialsHandler>.Instance);
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:quota");
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.QuotaReasonParameterName, "raise export center quota for tenant");
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.QuotaTicketParameterName, "CHG-7721");
SetParameter(transaction, AuthorityOpenIddictConstants.QuotaReasonParameterName, "raise export center quota for tenant");
SetParameter(transaction, AuthorityOpenIddictConstants.QuotaTicketParameterName, "CHG-7721");
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await handler.HandleAsync(context);
@@ -1481,7 +1481,7 @@ public class ClientCredentialsHandlersTests
var longReason = new string('a', 257);
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:quota");
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.QuotaReasonParameterName, longReason);
SetParameter(transaction, AuthorityOpenIddictConstants.QuotaReasonParameterName, longReason);
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await handler.HandleAsync(context);
@@ -1518,8 +1518,8 @@ public class ClientCredentialsHandlersTests
NullLogger<ValidateClientCredentialsHandler>.Instance);
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:quota");
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.QuotaReasonParameterName, "increase concurrency to unblock digests");
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.QuotaTicketParameterName, new string('b', 129));
SetParameter(transaction, AuthorityOpenIddictConstants.QuotaReasonParameterName, "increase concurrency to unblock digests");
SetParameter(transaction, AuthorityOpenIddictConstants.QuotaTicketParameterName, new string('b', 129));
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await handler.HandleAsync(context);
@@ -1556,7 +1556,7 @@ public class ClientCredentialsHandlersTests
NullLogger<ValidateClientCredentialsHandler>.Instance);
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:quota");
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.QuotaReasonParameterName, "grant five extra concurrent backfills");
SetParameter(transaction, AuthorityOpenIddictConstants.QuotaReasonParameterName, "grant five extra concurrent backfills");
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await handler.HandleAsync(context);
@@ -1598,8 +1598,8 @@ public class ClientCredentialsHandlersTests
NullLogger<ValidateClientCredentialsHandler>.Instance);
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:quota");
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.QuotaReasonParameterName, "temporary burst for export audit");
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.QuotaTicketParameterName, "RFC-5541");
SetParameter(transaction, AuthorityOpenIddictConstants.QuotaReasonParameterName, "temporary burst for export audit");
SetParameter(transaction, AuthorityOpenIddictConstants.QuotaTicketParameterName, "RFC-5541");
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await handler.HandleAsync(context);
@@ -1796,7 +1796,7 @@ public class ClientCredentialsHandlersTests
NullLogger<ValidateClientCredentialsHandler>.Instance);
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "export.admin");
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.ExportAdminTicketParameterName, "INC-9001");
SetParameter(transaction, AuthorityOpenIddictConstants.ExportAdminTicketParameterName, "INC-9001");
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await handler.HandleAsync(context);
@@ -1832,7 +1832,7 @@ public class ClientCredentialsHandlersTests
NullLogger<ValidateClientCredentialsHandler>.Instance);
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "export.admin");
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.ExportAdminReasonParameterName, "Rotate encryption keys after incident postmortem");
SetParameter(transaction, AuthorityOpenIddictConstants.ExportAdminReasonParameterName, "Rotate encryption keys after incident postmortem");
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await handler.HandleAsync(context);
@@ -1868,8 +1868,8 @@ public class ClientCredentialsHandlersTests
NullLogger<ValidateClientCredentialsHandler>.Instance);
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "export.admin");
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.ExportAdminReasonParameterName, "Rotate encryption keys after incident postmortem");
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.ExportAdminTicketParameterName, "INC-9001");
SetParameter(transaction, AuthorityOpenIddictConstants.ExportAdminReasonParameterName, "Rotate encryption keys after incident postmortem");
SetParameter(transaction, AuthorityOpenIddictConstants.ExportAdminTicketParameterName, "INC-9001");
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await handler.HandleAsync(context);
@@ -2310,7 +2310,7 @@ public class ClientCredentialsHandlersTests
NullLogger<ValidateClientCredentialsHandler>.Instance);
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "jobs:read");
transaction.Request?.SetParameter("unexpected_param", "value");
SetParameter(transaction,"unexpected_param", "value");
await handler.HandleAsync(new OpenIddictServerEvents.ValidateTokenRequestContext(transaction));
@@ -2522,8 +2522,9 @@ public class ClientCredentialsHandlersTests
Assert.True(validateContext.IsRejected);
Assert.Equal(OpenIddictConstants.Errors.InvalidClient, validateContext.Error);
var authenticateHeader = Assert.Single(httpContext.Response.Headers.Select(header => header)
.Where(header => string.Equals(header.Key, "WWW-Authenticate", StringComparison.OrdinalIgnoreCase))).Value;
var authenticateHeader = Assert.Single(
httpContext.Response.Headers,
header => string.Equals(header.Key, "WWW-Authenticate", StringComparison.OrdinalIgnoreCase)).Value;
Assert.Contains("use_dpop_nonce", authenticateHeader.ToString());
Assert.True(httpContext.Response.Headers.TryGetValue("DPoP-Nonce", out var nonceValues));
Assert.False(StringValues.IsNullOrEmpty(nonceValues));
@@ -2862,7 +2863,7 @@ public class ClientCredentialsHandlersTests
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "jobs:read");
transaction.Options.AccessTokenLifetime = TimeSpan.FromMinutes(10);
transaction.Request.SetParameter(AuthorityOpenIddictConstants.ServiceAccountParameterName, "svc-ops");
SetParameter(transaction, AuthorityOpenIddictConstants.ServiceAccountParameterName, "svc-ops");
var validateContext = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await validateHandler.HandleAsync(validateContext);
@@ -2951,10 +2952,10 @@ public class ClientCredentialsHandlersTests
NullLogger<ValidateClientCredentialsHandler>.Instance);
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "vuln:view vuln:investigate");
transaction.Request.SetParameter(AuthorityOpenIddictConstants.ServiceAccountParameterName, "svc-vuln");
transaction.Request.SetParameter(AuthorityOpenIddictConstants.VulnEnvironmentParameterName, "prod");
transaction.Request.SetParameter(AuthorityOpenIddictConstants.VulnOwnerParameterName, "secops");
transaction.Request.SetParameter(AuthorityOpenIddictConstants.VulnBusinessTierParameterName, "tier-1");
SetParameter(transaction, AuthorityOpenIddictConstants.ServiceAccountParameterName, "svc-vuln");
SetParameter(transaction, AuthorityOpenIddictConstants.VulnEnvironmentParameterName, "prod");
SetParameter(transaction, AuthorityOpenIddictConstants.VulnOwnerParameterName, "secops");
SetParameter(transaction, AuthorityOpenIddictConstants.VulnBusinessTierParameterName, "tier-1");
var validateContext = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await validateHandler.HandleAsync(validateContext);
@@ -3071,7 +3072,7 @@ public class TokenValidationHandlersTests
Request = new OpenIddictRequest()
};
var principal = CreatePrincipal(clientDocument.ClientId, "token-tenant", clientDocument.Plugin);
var principal = CreatePrincipal(clientDocument.ClientId, "token-tenant", ResolveProvider(clientDocument));
var context = new OpenIddictServerEvents.ValidateTokenContext(transaction)
{
Principal = principal,
@@ -3125,7 +3126,7 @@ public class TokenValidationHandlersTests
Request = new OpenIddictRequest()
};
var principal = CreatePrincipal(clientDocument.ClientId, "token-tenant", clientDocument.Plugin);
var principal = CreatePrincipal(clientDocument.ClientId, "token-tenant", ResolveProvider(clientDocument));
principal.Identities.First().AddClaim(new Claim(StellaOpsClaimTypes.Tenant, "tenant-beta"));
var context = new OpenIddictServerEvents.ValidateTokenContext(transaction)
{
@@ -3175,7 +3176,7 @@ public class TokenValidationHandlersTests
Request = new OpenIddictRequest()
};
var principal = CreatePrincipal(clientDocument.ClientId, "token-tenant", clientDocument.Plugin);
var principal = CreatePrincipal(clientDocument.ClientId, "token-tenant", ResolveProvider(clientDocument));
var context = new OpenIddictServerEvents.ValidateTokenContext(transaction)
{
Principal = principal,
@@ -3224,7 +3225,7 @@ public class TokenValidationHandlersTests
Request = new OpenIddictRequest()
};
var principal = CreatePrincipal(clientDocument.ClientId, "token-tenant", clientDocument.Plugin);
var principal = CreatePrincipal(clientDocument.ClientId, "token-tenant", ResolveProvider(clientDocument));
principal.Identities.First().AddClaim(new Claim(StellaOpsClaimTypes.Tenant, "tenant-alpha"));
var context = new OpenIddictServerEvents.ValidateTokenContext(transaction)
{
@@ -3326,7 +3327,7 @@ public class TokenValidationHandlersTests
Request = new OpenIddictRequest()
};
var principal = CreatePrincipal(clientDocument.ClientId, tokenDocument.TokenId, clientDocument.Plugin);
var principal = CreatePrincipal(clientDocument.ClientId, tokenDocument.TokenId, ResolveProvider(clientDocument));
var context = new OpenIddictServerEvents.ValidateTokenContext(transaction)
{
Principal = principal,
@@ -3963,7 +3964,7 @@ public class ObservabilityIncidentTokenHandlerTests
Request = new OpenIddictRequest()
};
var principal = CreatePrincipal(clientDocument.ClientId, "token-incident", clientDocument.Plugin);
var principal = CreatePrincipal(clientDocument.ClientId, "token-incident", ResolveProvider(clientDocument));
principal.SetScopes(StellaOpsScopes.ObservabilityIncident);
var staleAuthTime = DateTimeOffset.UtcNow.AddMinutes(-10);
principal.SetClaim(OpenIddictConstants.Claims.AuthenticationTime, staleAuthTime.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture));
@@ -4018,7 +4019,7 @@ public class ObservabilityIncidentTokenHandlerTests
Request = new OpenIddictRequest()
};
var principal = CreatePrincipal(clientDocument.ClientId, "token-policy", clientDocument.Plugin);
var principal = CreatePrincipal(clientDocument.ClientId, "token-policy", ResolveProvider(clientDocument));
principal.SetScopes(StellaOpsScopes.PolicyPublish);
principal.SetClaim(StellaOpsClaimTypes.PolicyOperation, AuthorityOpenIddictConstants.PolicyOperationPublishValue);
principal.SetClaim(StellaOpsClaimTypes.PolicyReason, "Publish approved policy");
@@ -4075,7 +4076,7 @@ public class ObservabilityIncidentTokenHandlerTests
Request = new OpenIddictRequest()
};
var principal = CreatePrincipal(clientDocument.ClientId, "token-policy-stale", clientDocument.Plugin);
var principal = CreatePrincipal(clientDocument.ClientId, "token-policy-stale", ResolveProvider(clientDocument));
principal.SetScopes(StellaOpsScopes.PolicyPublish);
principal.SetClaim(StellaOpsClaimTypes.PolicyOperation, AuthorityOpenIddictConstants.PolicyOperationPublishValue);
principal.SetClaim(StellaOpsClaimTypes.PolicyDigest, new string('a', 64));
@@ -4136,7 +4137,7 @@ public class ObservabilityIncidentTokenHandlerTests
Request = new OpenIddictRequest()
};
var principal = CreatePrincipal(clientDocument.ClientId, $"token-{expectedOperation}", clientDocument.Plugin);
var principal = CreatePrincipal(clientDocument.ClientId, $"token-{expectedOperation}", ResolveProvider(clientDocument));
principal.SetScopes(scope);
principal.SetClaim(StellaOpsClaimTypes.PolicyOperation, expectedOperation);
principal.SetClaim(StellaOpsClaimTypes.PolicyDigest, new string('b', 64));
@@ -4245,6 +4246,27 @@ internal static class TestHelpers
private static string? NormalizeTenant(string? value)
=> string.IsNullOrWhiteSpace(value) ? null : value.Trim().ToLowerInvariant();
public static string ResolveProvider(AuthorityClientDocument document)
=> string.IsNullOrWhiteSpace(document.Plugin) ? "standard" : document.Plugin;
private static OpenIddictRequest GetRequest(OpenIddictServerTransaction transaction)
=> transaction.Request ?? throw new InvalidOperationException("OpenIddict request is required for this test.");
public static void SetParameter(OpenIddictServerTransaction transaction, string name, object? value)
{
var parameter = value switch
{
null => default,
OpenIddictParameter existing => existing,
string s => new OpenIddictParameter(s),
bool b => new OpenIddictParameter(b),
int i => new OpenIddictParameter(i),
long l => new OpenIddictParameter(l),
_ => new OpenIddictParameter(value?.ToString())
};
GetRequest(transaction).SetParameter(name, parameter);
}
public static AuthorityClientDescriptor CreateDescriptor(AuthorityClientDocument document)
{
var allowedGrantTypes = document.Properties.TryGetValue(AuthorityClientMetadataKeys.AllowedGrantTypes, out var grants) ? grants?.Split(' ', StringSplitOptions.RemoveEmptyEntries) : Array.Empty<string>();

View File

@@ -21,11 +21,11 @@ public sealed class DiscoveryMetadataTests : IClassFixture<AuthorityWebApplicati
{
using var client = factory.CreateClient();
using var response = await client.GetAsync("/.well-known/openid-configuration").ConfigureAwait(false);
using var response = await client.GetAsync("/.well-known/openid-configuration");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var payload = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
var payload = await response.Content.ReadAsStringAsync();
using var document = JsonDocument.Parse(payload);
var root = document.RootElement;

View File

@@ -40,7 +40,7 @@ public sealed class LegacyAuthDeprecationTests : IClassFixture<AuthorityWebAppli
new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "client_credentials"
})).ConfigureAwait(false);
}));
Assert.NotNull(response);
Assert.True(response.Headers.TryGetValues("Deprecation", out var deprecationValues));
@@ -77,7 +77,7 @@ public sealed class LegacyAuthDeprecationTests : IClassFixture<AuthorityWebAppli
new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "client_credentials"
})).ConfigureAwait(false);
}));
Assert.NotNull(response);

View File

@@ -122,7 +122,7 @@ public class PasswordGrantHandlersTests
var handle = new HandlePasswordGrantHandler(registry, clientStore, TestActivitySource, sink, metadataAccessor, TimeProvider.System, NullLogger<HandlePasswordGrantHandler>.Instance);
var transaction = CreatePasswordTransaction("alice", "Password1!", "obs:incident");
transaction.Request.SetParameter("incident_reason", "Sev1 drill activation");
SetParameter(transaction, "incident_reason", "Sev1 drill activation");
var validateContext = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await validate.HandleAsync(validateContext);
@@ -213,8 +213,8 @@ public class PasswordGrantHandlersTests
var validate = new ValidatePasswordGrantHandler(registry, TestActivitySource, sink, metadataAccessor, clientStore, TimeProvider.System, NullLogger<ValidatePasswordGrantHandler>.Instance);
var transaction = CreatePasswordTransaction("alice", "Password1!", "policy:publish");
transaction.Request.SetParameter("policy_ticket", "CR-1001");
transaction.Request.SetParameter("policy_digest", new string('a', 64));
SetParameter(transaction, "policy_ticket", "CR-1001");
SetParameter(transaction, "policy_digest", new string('a', 64));
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await validate.HandleAsync(context);
@@ -239,8 +239,8 @@ public class PasswordGrantHandlersTests
var validate = new ValidatePasswordGrantHandler(registry, TestActivitySource, sink, metadataAccessor, clientStore, TimeProvider.System, NullLogger<ValidatePasswordGrantHandler>.Instance);
var transaction = CreatePasswordTransaction("alice", "Password1!", "policy:publish");
transaction.Request.SetParameter("policy_reason", "Publish approved policy");
transaction.Request.SetParameter("policy_digest", new string('b', 64));
SetParameter(transaction, "policy_reason", "Publish approved policy");
SetParameter(transaction, "policy_digest", new string('b', 64));
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await validate.HandleAsync(context);
@@ -261,8 +261,8 @@ public class PasswordGrantHandlersTests
var validate = new ValidatePasswordGrantHandler(registry, TestActivitySource, sink, metadataAccessor, clientStore, TimeProvider.System, NullLogger<ValidatePasswordGrantHandler>.Instance);
var transaction = CreatePasswordTransaction("alice", "Password1!", "policy:publish");
transaction.Request.SetParameter("policy_reason", "Publish approved policy");
transaction.Request.SetParameter("policy_ticket", "CR-1002");
SetParameter(transaction, "policy_reason", "Publish approved policy");
SetParameter(transaction, "policy_ticket", "CR-1002");
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await validate.HandleAsync(context);
@@ -283,9 +283,9 @@ public class PasswordGrantHandlersTests
var validate = new ValidatePasswordGrantHandler(registry, TestActivitySource, sink, metadataAccessor, clientStore, TimeProvider.System, NullLogger<ValidatePasswordGrantHandler>.Instance);
var transaction = CreatePasswordTransaction("alice", "Password1!", "policy:publish");
transaction.Request.SetParameter("policy_reason", "Publish approved policy");
transaction.Request.SetParameter("policy_ticket", "CR-1003");
transaction.Request.SetParameter("policy_digest", "not-hex");
SetParameter(transaction, "policy_reason", "Publish approved policy");
SetParameter(transaction, "policy_ticket", "CR-1003");
SetParameter(transaction, "policy_digest", "not-hex");
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await validate.HandleAsync(context);
@@ -311,9 +311,9 @@ public class PasswordGrantHandlersTests
var handle = new HandlePasswordGrantHandler(registry, clientStore, TestActivitySource, sink, metadataAccessor, TimeProvider.System, NullLogger<HandlePasswordGrantHandler>.Instance);
var transaction = CreatePasswordTransaction("alice", "Password1!", scope);
transaction.Request.SetParameter("policy_reason", "Promote approved policy");
transaction.Request.SetParameter("policy_ticket", "CR-1004");
transaction.Request.SetParameter("policy_digest", new string('c', 64));
SetParameter(transaction, "policy_reason", "Promote approved policy");
SetParameter(transaction, "policy_ticket", "CR-1004");
SetParameter(transaction, "policy_digest", new string('c', 64));
var validateContext = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await validate.HandleAsync(validateContext);
@@ -403,7 +403,7 @@ public class PasswordGrantHandlersTests
var validate = new ValidatePasswordGrantHandler(registry, TestActivitySource, sink, metadataAccessor, clientStore, TimeProvider.System, NullLogger<ValidatePasswordGrantHandler>.Instance);
var transaction = CreatePasswordTransaction("alice", "Password1!");
transaction.Request?.SetParameter("unexpected_param", "value");
SetParameter(transaction, "unexpected_param", "value");
await validate.HandleAsync(new OpenIddictServerEvents.ValidateTokenRequestContext(transaction));
@@ -506,6 +506,22 @@ public class PasswordGrantHandlersTests
};
}
private static void SetParameter(OpenIddictServerTransaction transaction, string name, object? value)
{
var request = transaction.Request ?? throw new InvalidOperationException("OpenIddict request is required for this test.");
var parameter = value switch
{
null => default,
OpenIddictParameter existing => existing,
string s => new OpenIddictParameter(s),
bool b => new OpenIddictParameter(b),
int i => new OpenIddictParameter(i),
long l => new OpenIddictParameter(l),
_ => new OpenIddictParameter(value?.ToString())
};
request.SetParameter(name, parameter);
}
private static StellaOpsAuthorityOptions CreateAuthorityOptions(Action<StellaOpsAuthorityOptions>? configure = null)
{
var options = new StellaOpsAuthorityOptions

View File

@@ -3,15 +3,16 @@ using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using StellaOps.Authority.RateLimiting;
using StellaOps.Configuration;
using Xunit;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Hosting;
using Microsoft.AspNetCore.Hosting;
using StellaOps.Authority.RateLimiting;
using StellaOps.Configuration;
using Xunit;
namespace StellaOps.Authority.Tests.RateLimiting;
@@ -91,46 +92,52 @@ public class AuthorityRateLimiterIntegrationTests
configure?.Invoke(options);
var builder = new WebHostBuilder()
.ConfigureServices(services =>
{
services.AddSingleton(options);
services.AddSingleton<IOptions<StellaOpsAuthorityOptions>>(Options.Create(options));
services.TryAddSingleton<TimeProvider>(_ => TimeProvider.System);
services.AddHttpContextAccessor();
services.TryAddSingleton<IAuthorityRateLimiterMetadataAccessor, AuthorityRateLimiterMetadataAccessor>();
services.TryAddSingleton<IAuthorityRateLimiterPartitionKeyResolver, DefaultAuthorityRateLimiterPartitionKeyResolver>();
services.AddRateLimiter(rateLimiterOptions =>
{
AuthorityRateLimiter.Configure(rateLimiterOptions, options);
});
})
.Configure(app =>
{
app.UseAuthorityRateLimiterContext();
app.UseRateLimiter();
app.Map("/token", builder =>
{
builder.Run(async context =>
{
context.Response.StatusCode = StatusCodes.Status200OK;
await context.Response.WriteAsync("token-ok");
});
});
app.Map("/internal/ping", builder =>
{
builder.Run(async context =>
{
context.Response.StatusCode = StatusCodes.Status200OK;
await context.Response.WriteAsync("internal-ok");
});
});
});
return new TestServer(builder);
}
var hostBuilder = new HostBuilder()
.ConfigureWebHost(web =>
{
web.UseTestServer();
web.ConfigureServices(services =>
{
services.AddSingleton(options);
services.AddSingleton<IOptions<StellaOpsAuthorityOptions>>(Options.Create(options));
services.TryAddSingleton<TimeProvider>(_ => TimeProvider.System);
services.AddHttpContextAccessor();
services.TryAddSingleton<IAuthorityRateLimiterMetadataAccessor, AuthorityRateLimiterMetadataAccessor>();
services.TryAddSingleton<IAuthorityRateLimiterPartitionKeyResolver, DefaultAuthorityRateLimiterPartitionKeyResolver>();
services.AddRateLimiter(rateLimiterOptions =>
{
AuthorityRateLimiter.Configure(rateLimiterOptions, options);
});
});
web.Configure(app =>
{
app.UseAuthorityRateLimiterContext();
app.UseRateLimiter();
app.Map("/token", tokenBuilder =>
{
tokenBuilder.Run(async context =>
{
context.Response.StatusCode = StatusCodes.Status200OK;
await context.Response.WriteAsync("token-ok");
});
});
app.Map("/internal/ping", internalBuilder =>
{
internalBuilder.Run(async context =>
{
context.Response.StatusCode = StatusCodes.Status200OK;
await context.Response.WriteAsync("internal-ok");
});
});
});
});
var host = hostBuilder.Start();
return host.GetTestServer() ?? throw new InvalidOperationException("Failed to create TestServer.");
}
private static FormUrlEncodedContent CreateTokenForm(string clientId)
=> new(new Dictionary<string, string>

View File

@@ -28,6 +28,11 @@ namespace StellaOps.Authority.Tests.Vulnerability;
public sealed class VulnWorkflowTokenEndpointTests : IClassFixture<AuthorityWebApplicationFactory>
{
private readonly AuthorityWebApplicationFactory factory;
private const string SigningEnabledKey = "STELLAOPS_AUTHORITY_AUTHORITY__SIGNING__ENABLED";
private const string SigningActiveKeyIdKey = "STELLAOPS_AUTHORITY_AUTHORITY__SIGNING__ACTIVEKEYID";
private const string SigningKeyPathKey = "STELLAOPS_AUTHORITY_AUTHORITY__SIGNING__KEYPATH";
private const string SigningKeySourceKey = "STELLAOPS_AUTHORITY_AUTHORITY__SIGNING__KEYSOURCE";
private const string SigningAlgorithmKey = "STELLAOPS_AUTHORITY_AUTHORITY__SIGNING__ALGORITHM";
public VulnWorkflowTokenEndpointTests(AuthorityWebApplicationFactory factory)
{
@@ -44,6 +49,15 @@ public sealed class VulnWorkflowTokenEndpointTests : IClassFixture<AuthorityWebA
{
CreateEcPrivateKey(keyPath);
using var env = new EnvironmentVariableScope(new[]
{
new KeyValuePair<string, string?>(SigningEnabledKey, "true"),
new KeyValuePair<string, string?>(SigningActiveKeyIdKey, "workflow-key"),
new KeyValuePair<string, string?>(SigningKeyPathKey, keyPath),
new KeyValuePair<string, string?>(SigningKeySourceKey, "file"),
new KeyValuePair<string, string?>(SigningAlgorithmKey, SignatureAlgorithms.Es256)
});
var sink = new RecordingAuthEventSink();
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T09:00:00Z"));
@@ -94,10 +108,10 @@ public sealed class VulnWorkflowTokenEndpointTests : IClassFixture<AuthorityWebA
Assert.Equal("tenant-default", verified!.Tenant);
Assert.Equal("workflow-nonce-123456", verified.Nonce);
var issuedEvent = Assert.Single(sink.Events.Where(evt => evt.EventType == "vuln.workflow.csrf.issued"));
var issuedEvent = Assert.Single(sink.Events, evt => evt.EventType == "vuln.workflow.csrf.issued");
Assert.Contains(issuedEvent.Properties, property => property.Name == "vuln.workflow.actor");
var verifiedEvent = Assert.Single(sink.Events.Where(evt => evt.EventType == "vuln.workflow.csrf.verified"));
var verifiedEvent = Assert.Single(sink.Events, evt => evt.EventType == "vuln.workflow.csrf.verified");
Assert.Contains(verifiedEvent.Properties, property => property.Name == "vuln.workflow.nonce" && property.Value.Value == "workflow-nonce-123456");
}
finally
@@ -116,6 +130,15 @@ public sealed class VulnWorkflowTokenEndpointTests : IClassFixture<AuthorityWebA
{
CreateEcPrivateKey(keyPath);
using var env = new EnvironmentVariableScope(new[]
{
new KeyValuePair<string, string?>(SigningEnabledKey, "true"),
new KeyValuePair<string, string?>(SigningActiveKeyIdKey, "workflow-key"),
new KeyValuePair<string, string?>(SigningKeyPathKey, keyPath),
new KeyValuePair<string, string?>(SigningKeySourceKey, "file"),
new KeyValuePair<string, string?>(SigningAlgorithmKey, SignatureAlgorithms.Es256)
});
var sink = new RecordingAuthEventSink();
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T09:10:00Z"));
@@ -158,6 +181,15 @@ public sealed class VulnWorkflowTokenEndpointTests : IClassFixture<AuthorityWebA
{
CreateEcPrivateKey(keyPath);
using var env = new EnvironmentVariableScope(new[]
{
new KeyValuePair<string, string?>(SigningEnabledKey, "true"),
new KeyValuePair<string, string?>(SigningActiveKeyIdKey, "workflow-key"),
new KeyValuePair<string, string?>(SigningKeyPathKey, keyPath),
new KeyValuePair<string, string?>(SigningKeySourceKey, "file"),
new KeyValuePair<string, string?>(SigningAlgorithmKey, SignatureAlgorithms.Es256)
});
var sink = new RecordingAuthEventSink();
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T09:20:00Z"));
@@ -196,7 +228,7 @@ public sealed class VulnWorkflowTokenEndpointTests : IClassFixture<AuthorityWebA
Assert.Equal("invalid_token", error!["error"]);
Assert.Contains("Token does not permit action", error["message"], StringComparison.Ordinal);
Assert.Single(sink.Events.Where(evt => evt.EventType == "vuln.workflow.csrf.issued"));
Assert.Single(sink.Events, evt => evt.EventType == "vuln.workflow.csrf.issued");
Assert.DoesNotContain(sink.Events, evt => evt.EventType == "vuln.workflow.csrf.verified");
}
finally
@@ -215,6 +247,15 @@ public sealed class VulnWorkflowTokenEndpointTests : IClassFixture<AuthorityWebA
{
CreateEcPrivateKey(keyPath);
using var env = new EnvironmentVariableScope(new[]
{
new KeyValuePair<string, string?>(SigningEnabledKey, "true"),
new KeyValuePair<string, string?>(SigningActiveKeyIdKey, "attachment-key"),
new KeyValuePair<string, string?>(SigningKeyPathKey, keyPath),
new KeyValuePair<string, string?>(SigningKeySourceKey, "file"),
new KeyValuePair<string, string?>(SigningAlgorithmKey, SignatureAlgorithms.Es256)
});
var sink = new RecordingAuthEventSink();
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T11:00:00Z"));
@@ -233,7 +274,7 @@ public sealed class VulnWorkflowTokenEndpointTests : IClassFixture<AuthorityWebA
FindingId = "find-456",
ContentHash = "sha256:abc123",
ContentType = "application/pdf",
Metadata = new Dictionary<string, string> { ["origin"] = "vuln-workflow" }
Metadata = new Dictionary<string, string?> { ["origin"] = "vuln-workflow" }
};
var issueResponse = await client.PostAsJsonAsync("/vuln/attachments/tokens/issue", issuePayload);
@@ -256,10 +297,10 @@ public sealed class VulnWorkflowTokenEndpointTests : IClassFixture<AuthorityWebA
Assert.NotNull(verified);
Assert.Equal("ledger-hash-001", verified!.LedgerEventHash);
var issuedEvent = Assert.Single(sink.Events.Where(evt => evt.EventType == "vuln.attachment.token.issued"));
var issuedEvent = Assert.Single(sink.Events, evt => evt.EventType == "vuln.attachment.token.issued");
Assert.Contains(issuedEvent.Properties, property => property.Name == "vuln.attachment.ledger_hash" && property.Value.Value == "ledger-hash-001");
var verifiedEvent = Assert.Single(sink.Events.Where(evt => evt.EventType == "vuln.attachment.token.verified"));
var verifiedEvent = Assert.Single(sink.Events, evt => evt.EventType == "vuln.attachment.token.verified");
Assert.Contains(verifiedEvent.Properties, property => property.Name == "vuln.attachment.ledger_hash" && property.Value.Value == "ledger-hash-001");
}
finally
@@ -278,6 +319,15 @@ public sealed class VulnWorkflowTokenEndpointTests : IClassFixture<AuthorityWebA
{
CreateEcPrivateKey(keyPath);
using var env = new EnvironmentVariableScope(new[]
{
new KeyValuePair<string, string?>(SigningEnabledKey, "true"),
new KeyValuePair<string, string?>(SigningActiveKeyIdKey, "attachment-key"),
new KeyValuePair<string, string?>(SigningKeyPathKey, keyPath),
new KeyValuePair<string, string?>(SigningKeySourceKey, "file"),
new KeyValuePair<string, string?>(SigningAlgorithmKey, SignatureAlgorithms.Es256)
});
var sink = new RecordingAuthEventSink();
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T11:10:00Z"));
@@ -316,7 +366,7 @@ public sealed class VulnWorkflowTokenEndpointTests : IClassFixture<AuthorityWebA
Assert.Equal("invalid_token", error!["error"]);
Assert.Contains("ledger reference", error["message"], StringComparison.OrdinalIgnoreCase);
Assert.Single(sink.Events.Where(evt => evt.EventType == "vuln.attachment.token.issued"));
Assert.Single(sink.Events, evt => evt.EventType == "vuln.attachment.token.issued");
Assert.DoesNotContain(sink.Events, evt => evt.EventType == "vuln.attachment.token.verified");
}
finally

View File

@@ -134,6 +134,7 @@
> 2025-11-02: Authority bootstrap test harness now seeds service accounts via AuthorityDelegation options; `/internal/service-accounts` endpoints validated with targeted vstest run.
> 2025-11-02: Added Mongo service-account store, seeded options/collection initializers, token persistence metadata (`tokenKind`, `serviceAccountId`, `actorChain`), and docs/config samples. Introduced quota checks + tests covering service account issuance and persistence.
> 2025-11-02: Documented bootstrap service-account admin APIs in `docs/11_AUTHORITY.md`, noting API key requirements and stable upsert behaviour.
> 2025-11-03: Seeded explicit enabled service-account fixtures for integration tests and reran `StellaOps.Authority.Tests` to greenlight `/internal/service-accounts` listing + revocation scenarios.
## Observability & Forensics (Epic 15)