feat: Enhance Authority Identity Provider Registry with Bootstrap Capability

- Added support for bootstrap providers in AuthorityIdentityProviderRegistry.
- Introduced a new property for bootstrap providers and updated AggregateCapabilities.
- Updated relevant methods to handle bootstrap capabilities during provider registration.

feat: Introduce Sealed Mode Status in OpenIddict Handlers

- Added SealedModeStatusProperty to AuthorityOpenIddictConstants.
- Enhanced ValidateClientCredentialsHandler, ValidatePasswordGrantHandler, and ValidateRefreshTokenGrantHandler to validate sealed mode evidence.
- Implemented logic to handle airgap seal confirmation requirements.

feat: Update Program Configuration for Sealed Mode

- Registered IAuthoritySealedModeEvidenceValidator in Program.cs.
- Added logging for bootstrap capabilities in identity provider plugins.
- Implemented checks for bootstrap support in API endpoints.

chore: Update Tasks and Documentation

- Marked AUTH-MTLS-11-002 as DONE in TASKS.md.
- Updated documentation to reflect changes in sealed mode and bootstrap capabilities.

fix: Improve CLI Command Handlers Output

- Enhanced output formatting for command responses and prompts in CommandHandlers.cs.

feat: Extend Advisory AI Models

- Added Response property to AdvisoryPipelineOutputModel for better output handling.

fix: Adjust Concelier Web Service Authentication

- Improved JWT token handling in Concelier Web Service to ensure proper token extraction and logging.

test: Enhance Web Service Endpoints Tests

- Added detailed logging for authentication failures in WebServiceEndpointsTests.
- Enabled PII logging for better debugging of authentication issues.

feat: Introduce Air-Gap Configuration Options

- Added AuthorityAirGapOptions and AuthoritySealedModeOptions to StellaOpsAuthorityOptions.
- Implemented validation logic for air-gap configurations to ensure proper setup.
This commit is contained in:
master
2025-11-09 12:18:14 +02:00
parent d71c81e45d
commit ba4c935182
68 changed files with 2142 additions and 291 deletions

View File

@@ -55,14 +55,24 @@ public class StandardPluginRegistrarTests
"standard.yaml");
var pluginContext = new AuthorityPluginContext(manifest, configuration);
var services = new ServiceCollection();
services.AddLogging();
services.AddSingleton<IMongoDatabase>(database);
services.AddSingleton<IAuthorityClientStore>(new InMemoryClientStore());
services.AddSingleton<IAuthorityRevocationStore>(new StubRevocationStore());
services.AddSingleton(TimeProvider.System);
services.AddSingleton<IAuthorityRevocationStore>(new StubRevocationStore());
services.AddSingleton(TimeProvider.System);
var services = new ServiceCollection();
services.AddLogging();
services.AddSingleton<IMongoDatabase>(database);
services.AddSingleton<IAuthorityClientStore>(new InMemoryClientStore());
services.AddSingleton<IAuthorityRevocationStore>(new StubRevocationStore());
services.AddSingleton(TimeProvider.System);
services.AddSingleton<IAuthorityLoginAttemptStore>(sp =>
{
var mongo = sp.GetRequiredService<IMongoDatabase>();
var collection = mongo.GetCollection<AuthorityLoginAttemptDocument>("authority_login_attempts");
return new AuthorityLoginAttemptStore(collection, NullLogger<AuthorityLoginAttemptStore>.Instance);
});
services.AddSingleton<IAuthorityLoginAttemptStore>(sp =>
{
var mongo = sp.GetRequiredService<IMongoDatabase>();
var collection = mongo.GetCollection<AuthorityLoginAttemptDocument>("authority_login_attempts");
return new AuthorityLoginAttemptStore(collection, NullLogger<AuthorityLoginAttemptStore>.Instance);
});
services.AddSingleton<IAuthorityRevocationStore>(new StubRevocationStore());
services.AddSingleton(TimeProvider.System);
services.AddSingleton<IAuthorityRevocationStore>(new StubRevocationStore());
@@ -91,7 +101,9 @@ public class StandardPluginRegistrarTests
using var scope = provider.CreateScope();
var plugin = scope.ServiceProvider.GetRequiredService<IIdentityProviderPlugin>();
Assert.Equal("standard", plugin.Type);
Assert.True(plugin.Capabilities.SupportsPassword);
Assert.True(plugin.Capabilities.SupportsPassword);
Assert.True(plugin.Capabilities.SupportsBootstrap);
Assert.True(plugin.Capabilities.SupportsClientProvisioning);
Assert.False(plugin.Capabilities.SupportsMfa);
Assert.True(plugin.Capabilities.SupportsClientProvisioning);
@@ -181,7 +193,9 @@ public class StandardPluginRegistrarTests
using var scope = provider.CreateScope();
var plugin = scope.ServiceProvider.GetRequiredService<IIdentityProviderPlugin>();
Assert.True(plugin.Capabilities.SupportsPassword);
Assert.True(plugin.Capabilities.SupportsPassword);
Assert.True(plugin.Capabilities.SupportsBootstrap);
Assert.True(plugin.Capabilities.SupportsClientProvisioning);
}
[Fact]

View File

@@ -19,6 +19,7 @@ public class StandardUserCredentialStoreTests : IAsyncLifetime
private readonly IMongoDatabase database;
private readonly StandardPluginOptions options;
private readonly StandardUserCredentialStore store;
private readonly TestAuditLogger auditLogger;
public StandardUserCredentialStoreTests()
{
@@ -50,17 +51,20 @@ public class StandardUserCredentialStoreTests : IAsyncLifetime
}
};
var cryptoProvider = new DefaultCryptoProvider();
auditLogger = new TestAuditLogger();
store = new StandardUserCredentialStore(
"standard",
database,
options,
new CryptoPasswordHasher(options, cryptoProvider),
auditLogger,
NullLogger<StandardUserCredentialStore>.Instance);
}
[Fact]
public async Task VerifyPasswordAsync_ReturnsSuccess_ForValidCredentials()
{
auditLogger.Reset();
var registration = new AuthorityUserRegistration(
"alice",
"Password1!",
@@ -77,11 +81,17 @@ public class StandardUserCredentialStoreTests : IAsyncLifetime
Assert.True(result.Succeeded);
Assert.Equal("alice", result.User?.Username);
Assert.Empty(result.AuditProperties);
var auditEntry = Assert.Single(auditLogger.Events);
Assert.Equal("alice", auditEntry.Username);
Assert.True(auditEntry.Success);
Assert.Null(auditEntry.FailureCode);
}
[Fact]
public async Task VerifyPasswordAsync_EnforcesLockout_AfterRepeatedFailures()
{
auditLogger.Reset();
await store.UpsertUserAsync(
new AuthorityUserRegistration(
"bob",
@@ -103,11 +113,18 @@ public class StandardUserCredentialStoreTests : IAsyncLifetime
Assert.NotNull(second.RetryAfter);
Assert.True(second.RetryAfter.Value > System.TimeSpan.Zero);
Assert.Contains(second.AuditProperties, property => property.Name == "plugin.lockout_until");
Assert.Equal(2, auditLogger.Events.Count);
Assert.False(auditLogger.Events[0].Success);
Assert.Equal(AuthorityCredentialFailureCode.InvalidCredentials, auditLogger.Events[0].FailureCode);
Assert.False(auditLogger.Events[1].Success);
Assert.Equal(AuthorityCredentialFailureCode.LockedOut, auditLogger.Events[1].FailureCode);
}
[Fact]
public async Task VerifyPasswordAsync_RehashesLegacyHashesToArgon2()
{
auditLogger.Reset();
var legacyHash = new Pbkdf2PasswordHasher().Hash(
"Legacy1!",
new PasswordHashOptions
@@ -136,6 +153,10 @@ public class StandardUserCredentialStoreTests : IAsyncLifetime
Assert.Equal("legacy", result.User?.Username);
Assert.Contains(result.AuditProperties, property => property.Name == "plugin.rehashed");
var auditEntry = Assert.Single(auditLogger.Events);
Assert.True(auditEntry.Success);
Assert.Equal("legacy", auditEntry.Username);
var updated = await database.GetCollection<StandardUserDocument>("authority_users_standard")
.Find(u => u.NormalizedUsername == "legacy")
.FirstOrDefaultAsync();
@@ -144,6 +165,23 @@ public class StandardUserCredentialStoreTests : IAsyncLifetime
Assert.StartsWith("$argon2id$", updated!.PasswordHash, StringComparison.Ordinal);
}
[Fact]
public async Task VerifyPasswordAsync_RecordsAudit_ForUnknownUser()
{
auditLogger.Reset();
var result = await store.VerifyPasswordAsync("unknown", "bad", CancellationToken.None);
Assert.False(result.Succeeded);
Assert.Equal(AuthorityCredentialFailureCode.InvalidCredentials, result.FailureCode);
var auditEntry = Assert.Single(auditLogger.Events);
Assert.Equal("unknown", auditEntry.Username);
Assert.False(auditEntry.Success);
Assert.Equal(AuthorityCredentialFailureCode.InvalidCredentials, auditEntry.FailureCode);
Assert.Equal("Invalid credentials.", auditEntry.Reason);
}
public Task InitializeAsync() => Task.CompletedTask;
public Task DisposeAsync()
@@ -152,3 +190,28 @@ public class StandardUserCredentialStoreTests : IAsyncLifetime
return Task.CompletedTask;
}
}
internal sealed class TestAuditLogger : IStandardCredentialAuditLogger
{
private readonly List<AuditEntry> events = new();
public IReadOnlyList<AuditEntry> Events => events;
public void Reset() => events.Clear();
public ValueTask RecordAsync(
string pluginName,
string normalizedUsername,
string? subjectId,
bool success,
AuthorityCredentialFailureCode? failureCode,
string? reason,
IReadOnlyList<AuthEventProperty> properties,
CancellationToken cancellationToken)
{
events.Add(new AuditEntry(normalizedUsername, success, failureCode, reason));
return ValueTask.CompletedTask;
}
internal sealed record AuditEntry(string Username, bool Success, AuthorityCredentialFailureCode? FailureCode, string? Reason);
}