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:
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user