Add authority bootstrap flows and Concelier ops runbooks

This commit is contained in:
2025-10-15 10:03:56 +03:00
parent ea8226120c
commit 0ba025022f
276 changed files with 21674 additions and 934 deletions

View File

@@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using MongoDB.Driver;
using MongoDB.Bson;
using OpenIddict.Abstractions;
using OpenIddict.Server;
using StellaOps.Authority;
@@ -56,10 +57,10 @@ public sealed class TokenPersistenceIntegrationTests
withClientProvisioning: true,
clientDescriptor: TestHelpers.CreateDescriptor(clientDocument));
var validateHandler = new ValidateClientCredentialsHandler(clientStore, registry, TestActivitySource, NullLogger<ValidateClientCredentialsHandler>.Instance);
var authSink = new TestAuthEventSink();
var metadataAccessor = new TestRateLimiterMetadataAccessor();
var handleHandler = new HandleClientCredentialsHandler(registry, TestActivitySource, authSink, metadataAccessor, clock, NullLogger<HandleClientCredentialsHandler>.Instance);
var validateHandler = new ValidateClientCredentialsHandler(clientStore, registry, TestActivitySource, authSink, metadataAccessor, clock, NullLogger<ValidateClientCredentialsHandler>.Instance);
var handleHandler = new HandleClientCredentialsHandler(registry, tokenStore, clock, TestActivitySource, NullLogger<HandleClientCredentialsHandler>.Instance);
var persistHandler = new PersistTokensHandler(tokenStore, clock, TestActivitySource, NullLogger<PersistTokensHandler>.Instance);
var transaction = TestHelpers.CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "jobs:trigger");
@@ -148,10 +149,14 @@ public sealed class TokenPersistenceIntegrationTests
var revokedAt = now.AddMinutes(1);
await tokenStore.UpdateStatusAsync(revokedTokenId, "revoked", revokedAt, "manual", null, null, CancellationToken.None);
var metadataAccessor = new TestRateLimiterMetadataAccessor();
var auditSink = new TestAuthEventSink();
var handler = new ValidateAccessTokenHandler(
tokenStore,
clientStore,
registry,
metadataAccessor,
auditSink,
clock,
TestActivitySource,
NullLogger<ValidateAccessTokenHandler>.Instance);
@@ -190,6 +195,60 @@ public sealed class TokenPersistenceIntegrationTests
Assert.Equal("manual", stored.RevokedReason);
}
[Fact]
public async Task RecordUsageAsync_FlagsSuspectedReplay_OnNewDeviceFingerprint()
{
await ResetCollectionsAsync();
var issuedAt = new DateTimeOffset(2025, 10, 14, 8, 0, 0, TimeSpan.Zero);
var clock = new FakeTimeProvider(issuedAt);
await using var provider = await BuildMongoProviderAsync(clock);
var tokenStore = provider.GetRequiredService<IAuthorityTokenStore>();
var tokenDocument = new AuthorityTokenDocument
{
TokenId = "token-replay",
Type = OpenIddictConstants.TokenTypeHints.AccessToken,
ClientId = "client-1",
Status = "valid",
CreatedAt = issuedAt,
Devices = new List<BsonDocument>
{
new BsonDocument
{
{ "remoteAddress", "10.0.0.1" },
{ "userAgent", "agent/1.0" },
{ "firstSeen", BsonDateTime.Create(issuedAt.AddMinutes(-10).UtcDateTime) },
{ "lastSeen", BsonDateTime.Create(issuedAt.AddMinutes(-5).UtcDateTime) },
{ "useCount", 2 }
}
}
};
await tokenStore.InsertAsync(tokenDocument, CancellationToken.None);
var result = await tokenStore.RecordUsageAsync(
"token-replay",
remoteAddress: "10.0.0.2",
userAgent: "agent/2.0",
observedAt: clock.GetUtcNow(),
CancellationToken.None);
Assert.Equal(TokenUsageUpdateStatus.SuspectedReplay, result.Status);
var stored = await tokenStore.FindByTokenIdAsync("token-replay", CancellationToken.None);
Assert.NotNull(stored);
Assert.Equal(2, stored!.Devices?.Count);
Assert.Contains(stored.Devices!, doc =>
{
var remote = doc.TryGetValue("remoteAddress", out var ra) && ra.IsString ? ra.AsString : null;
var agentValue = doc.TryGetValue("userAgent", out var ua) && ua.IsString ? ua.AsString : null;
return remote == "10.0.0.2" && agentValue == "agent/2.0";
});
}
private async Task ResetCollectionsAsync()
{
var tokens = fixture.Database.GetCollection<AuthorityTokenDocument>(AuthorityMongoDefaults.Collections.Tokens);
@@ -220,27 +279,3 @@ public sealed class TokenPersistenceIntegrationTests
return provider;
}
}
internal sealed class TestAuthEventSink : IAuthEventSink
{
public List<AuthEventRecord> Records { get; } = new();
public ValueTask WriteAsync(AuthEventRecord record, CancellationToken cancellationToken)
{
Records.Add(record);
return ValueTask.CompletedTask;
}
}
internal sealed class TestRateLimiterMetadataAccessor : IAuthorityRateLimiterMetadataAccessor
{
private readonly AuthorityRateLimiterMetadata metadata = new();
public AuthorityRateLimiterMetadata? GetMetadata() => metadata;
public void SetClientId(string? clientId) => metadata.ClientId = string.IsNullOrWhiteSpace(clientId) ? null : clientId;
public void SetSubjectId(string? subjectId) => metadata.SubjectId = string.IsNullOrWhiteSpace(subjectId) ? null : subjectId;
public void SetTag(string name, string? value) => metadata.SetTag(name, value);
}