Add authority bootstrap flows and Concelier ops runbooks
This commit is contained in:
		| @@ -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); | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user