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:
@@ -0,0 +1,132 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Authority.Plugins.Abstractions;
|
||||
using StellaOps.Authority.Storage.Mongo.Documents;
|
||||
using StellaOps.Authority.Storage.Mongo.Stores;
|
||||
using StellaOps.Cryptography.Audit;
|
||||
|
||||
namespace StellaOps.Authority.Plugin.Standard.Security;
|
||||
|
||||
internal interface IStandardCredentialAuditLogger
|
||||
{
|
||||
ValueTask RecordAsync(
|
||||
string pluginName,
|
||||
string normalizedUsername,
|
||||
string? subjectId,
|
||||
bool success,
|
||||
AuthorityCredentialFailureCode? failureCode,
|
||||
string? reason,
|
||||
IReadOnlyList<AuthEventProperty> properties,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
internal sealed class StandardCredentialAuditLogger : IStandardCredentialAuditLogger
|
||||
{
|
||||
private const string EventType = "authority.plugin.standard.password_verification";
|
||||
|
||||
private readonly IAuthorityLoginAttemptStore loginAttemptStore;
|
||||
private readonly TimeProvider timeProvider;
|
||||
private readonly ILogger<StandardCredentialAuditLogger> logger;
|
||||
|
||||
public StandardCredentialAuditLogger(
|
||||
IAuthorityLoginAttemptStore loginAttemptStore,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<StandardCredentialAuditLogger> logger)
|
||||
{
|
||||
this.loginAttemptStore = loginAttemptStore ?? throw new ArgumentNullException(nameof(loginAttemptStore));
|
||||
this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async ValueTask RecordAsync(
|
||||
string pluginName,
|
||||
string normalizedUsername,
|
||||
string? subjectId,
|
||||
bool success,
|
||||
AuthorityCredentialFailureCode? failureCode,
|
||||
string? reason,
|
||||
IReadOnlyList<AuthEventProperty> properties,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var document = new AuthorityLoginAttemptDocument
|
||||
{
|
||||
EventType = EventType,
|
||||
Outcome = NormalizeOutcome(success, failureCode),
|
||||
SubjectId = Normalize(subjectId),
|
||||
Username = Normalize(normalizedUsername),
|
||||
Plugin = pluginName,
|
||||
Successful = success,
|
||||
Reason = Normalize(reason),
|
||||
OccurredAt = timeProvider.GetUtcNow()
|
||||
};
|
||||
|
||||
if (properties.Count > 0)
|
||||
{
|
||||
document.Properties = ConvertProperties(properties);
|
||||
}
|
||||
|
||||
await loginAttemptStore.InsertAsync(document, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to record credential audit event for plugin {PluginName}.", pluginName);
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeOutcome(bool success, AuthorityCredentialFailureCode? failureCode)
|
||||
{
|
||||
if (success)
|
||||
{
|
||||
return "success";
|
||||
}
|
||||
|
||||
return failureCode switch
|
||||
{
|
||||
AuthorityCredentialFailureCode.LockedOut => "locked_out",
|
||||
AuthorityCredentialFailureCode.RequiresMfa => "requires_mfa",
|
||||
AuthorityCredentialFailureCode.RequiresPasswordReset => "requires_password_reset",
|
||||
AuthorityCredentialFailureCode.PasswordExpired => "password_expired",
|
||||
_ => "failure"
|
||||
};
|
||||
}
|
||||
|
||||
private static string? Normalize(string? value)
|
||||
=> string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
|
||||
private static List<AuthorityLoginAttemptPropertyDocument> ConvertProperties(
|
||||
IReadOnlyList<AuthEventProperty> properties)
|
||||
{
|
||||
if (properties.Count == 0)
|
||||
{
|
||||
return new List<AuthorityLoginAttemptPropertyDocument>();
|
||||
}
|
||||
|
||||
var documents = new List<AuthorityLoginAttemptPropertyDocument>(properties.Count);
|
||||
foreach (var property in properties)
|
||||
{
|
||||
if (property is null || string.IsNullOrWhiteSpace(property.Name) || !property.Value.HasValue)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
documents.Add(new AuthorityLoginAttemptPropertyDocument
|
||||
{
|
||||
Name = property.Name,
|
||||
Value = property.Value.Value,
|
||||
Classification = NormalizeClassification(property.Value.Classification)
|
||||
});
|
||||
}
|
||||
|
||||
return documents;
|
||||
}
|
||||
|
||||
private static string NormalizeClassification(AuthEventDataClassification classification)
|
||||
=> classification switch
|
||||
{
|
||||
AuthEventDataClassification.Personal => "personal",
|
||||
AuthEventDataClassification.Sensitive => "sensitive",
|
||||
_ => "none"
|
||||
};
|
||||
}
|
||||
@@ -32,7 +32,26 @@ internal sealed class StandardIdentityProviderPlugin : IIdentityProviderPlugin
|
||||
Context.Manifest.Name);
|
||||
}
|
||||
|
||||
Capabilities = manifestCapabilities with { SupportsPassword = true };
|
||||
if (!manifestCapabilities.SupportsBootstrap)
|
||||
{
|
||||
this.logger.LogWarning(
|
||||
"Standard Authority plugin '{PluginName}' manifest does not declare the 'bootstrap' capability. Forcing bootstrap support.",
|
||||
Context.Manifest.Name);
|
||||
}
|
||||
|
||||
if (!manifestCapabilities.SupportsClientProvisioning)
|
||||
{
|
||||
this.logger.LogWarning(
|
||||
"Standard Authority plugin '{PluginName}' manifest does not declare the 'clientProvisioning' capability. Forcing client provisioning support.",
|
||||
Context.Manifest.Name);
|
||||
}
|
||||
|
||||
Capabilities = manifestCapabilities with
|
||||
{
|
||||
SupportsPassword = true,
|
||||
SupportsBootstrap = true,
|
||||
SupportsClientProvisioning = true
|
||||
};
|
||||
}
|
||||
|
||||
public string Name => Context.Manifest.Name;
|
||||
|
||||
@@ -27,12 +27,12 @@ internal sealed class StandardPluginRegistrar : IAuthorityPluginRegistrar
|
||||
|
||||
var pluginName = context.Plugin.Manifest.Name;
|
||||
|
||||
context.Services.AddSingleton<StandardClaimsEnricher>();
|
||||
context.Services.AddSingleton<IClaimsEnricher>(sp => sp.GetRequiredService<StandardClaimsEnricher>());
|
||||
|
||||
context.Services.AddStellaOpsCrypto();
|
||||
|
||||
var configPath = context.Plugin.Manifest.ConfigPath;
|
||||
context.Services.AddSingleton<StandardClaimsEnricher>();
|
||||
context.Services.AddSingleton<IClaimsEnricher>(sp => sp.GetRequiredService<StandardClaimsEnricher>());
|
||||
|
||||
context.Services.AddStellaOpsCrypto();
|
||||
|
||||
var configPath = context.Plugin.Manifest.ConfigPath;
|
||||
|
||||
context.Services.AddOptions<StandardPluginOptions>(pluginName)
|
||||
.Bind(context.Plugin.Configuration)
|
||||
@@ -43,18 +43,21 @@ internal sealed class StandardPluginRegistrar : IAuthorityPluginRegistrar
|
||||
})
|
||||
.ValidateOnStart();
|
||||
|
||||
context.Services.AddScoped(sp =>
|
||||
{
|
||||
var database = sp.GetRequiredService<IMongoDatabase>();
|
||||
var optionsMonitor = sp.GetRequiredService<IOptionsMonitor<StandardPluginOptions>>();
|
||||
var pluginOptions = optionsMonitor.Get(pluginName);
|
||||
var cryptoProvider = sp.GetRequiredService<ICryptoProvider>();
|
||||
var passwordHasher = new CryptoPasswordHasher(pluginOptions, cryptoProvider);
|
||||
var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
|
||||
var registrarLogger = loggerFactory.CreateLogger<StandardPluginRegistrar>();
|
||||
|
||||
var baselinePolicy = new PasswordPolicyOptions();
|
||||
if (pluginOptions.PasswordPolicy.IsWeakerThan(baselinePolicy))
|
||||
context.Services.AddScoped<IStandardCredentialAuditLogger, StandardCredentialAuditLogger>();
|
||||
|
||||
context.Services.AddScoped(sp =>
|
||||
{
|
||||
var database = sp.GetRequiredService<IMongoDatabase>();
|
||||
var optionsMonitor = sp.GetRequiredService<IOptionsMonitor<StandardPluginOptions>>();
|
||||
var pluginOptions = optionsMonitor.Get(pluginName);
|
||||
var cryptoProvider = sp.GetRequiredService<ICryptoProvider>();
|
||||
var passwordHasher = new CryptoPasswordHasher(pluginOptions, cryptoProvider);
|
||||
var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
|
||||
var registrarLogger = loggerFactory.CreateLogger<StandardPluginRegistrar>();
|
||||
var auditLogger = sp.GetRequiredService<IStandardCredentialAuditLogger>();
|
||||
|
||||
var baselinePolicy = new PasswordPolicyOptions();
|
||||
if (pluginOptions.PasswordPolicy.IsWeakerThan(baselinePolicy))
|
||||
{
|
||||
registrarLogger.LogWarning(
|
||||
"Standard plugin '{Plugin}' configured a weaker password policy (minLength={Length}, requireUpper={Upper}, requireLower={Lower}, requireDigit={Digit}, requireSymbol={Symbol}) than the baseline (minLength={BaseLength}, requireUpper={BaseUpper}, requireLower={BaseLower}, requireDigit={BaseDigit}, requireSymbol={BaseSymbol}).",
|
||||
@@ -70,14 +73,15 @@ internal sealed class StandardPluginRegistrar : IAuthorityPluginRegistrar
|
||||
baselinePolicy.RequireDigit,
|
||||
baselinePolicy.RequireSymbol);
|
||||
}
|
||||
|
||||
return new StandardUserCredentialStore(
|
||||
pluginName,
|
||||
database,
|
||||
pluginOptions,
|
||||
passwordHasher,
|
||||
loggerFactory.CreateLogger<StandardUserCredentialStore>());
|
||||
});
|
||||
|
||||
return new StandardUserCredentialStore(
|
||||
pluginName,
|
||||
database,
|
||||
pluginOptions,
|
||||
passwordHasher,
|
||||
auditLogger,
|
||||
loggerFactory.CreateLogger<StandardUserCredentialStore>());
|
||||
});
|
||||
|
||||
context.Services.AddScoped(sp =>
|
||||
{
|
||||
|
||||
@@ -18,6 +18,7 @@ internal sealed class StandardUserCredentialStore : IUserCredentialStore
|
||||
private readonly IMongoCollection<StandardUserDocument> users;
|
||||
private readonly StandardPluginOptions options;
|
||||
private readonly IPasswordHasher passwordHasher;
|
||||
private readonly IStandardCredentialAuditLogger auditLogger;
|
||||
private readonly ILogger<StandardUserCredentialStore> logger;
|
||||
private readonly string pluginName;
|
||||
|
||||
@@ -26,11 +27,13 @@ internal sealed class StandardUserCredentialStore : IUserCredentialStore
|
||||
IMongoDatabase database,
|
||||
StandardPluginOptions options,
|
||||
IPasswordHasher passwordHasher,
|
||||
IStandardCredentialAuditLogger auditLogger,
|
||||
ILogger<StandardUserCredentialStore> logger)
|
||||
{
|
||||
this.pluginName = pluginName ?? throw new ArgumentNullException(nameof(pluginName));
|
||||
this.options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
this.passwordHasher = passwordHasher ?? throw new ArgumentNullException(nameof(passwordHasher));
|
||||
this.auditLogger = auditLogger ?? throw new ArgumentNullException(nameof(auditLogger));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
ArgumentNullException.ThrowIfNull(database);
|
||||
@@ -60,6 +63,14 @@ internal sealed class StandardUserCredentialStore : IUserCredentialStore
|
||||
if (user is null)
|
||||
{
|
||||
logger.LogWarning("Plugin {PluginName} failed password verification for unknown user {Username}.", pluginName, normalized);
|
||||
await RecordAuditAsync(
|
||||
normalized,
|
||||
subjectId: null,
|
||||
success: false,
|
||||
failureCode: AuthorityCredentialFailureCode.InvalidCredentials,
|
||||
reason: "Invalid credentials.",
|
||||
auditProperties,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return AuthorityCredentialVerificationResult.Failure(AuthorityCredentialFailureCode.InvalidCredentials, auditProperties: auditProperties);
|
||||
}
|
||||
|
||||
@@ -73,6 +84,15 @@ internal sealed class StandardUserCredentialStore : IUserCredentialStore
|
||||
Value = ClassifiedString.Public(lockoutEnd.ToString("O", CultureInfo.InvariantCulture))
|
||||
});
|
||||
|
||||
await RecordAuditAsync(
|
||||
normalized,
|
||||
user.SubjectId,
|
||||
success: false,
|
||||
failureCode: AuthorityCredentialFailureCode.LockedOut,
|
||||
reason: "Account is temporarily locked.",
|
||||
auditProperties,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return AuthorityCredentialVerificationResult.Failure(
|
||||
AuthorityCredentialFailureCode.LockedOut,
|
||||
"Account is temporarily locked.",
|
||||
@@ -111,6 +131,14 @@ internal sealed class StandardUserCredentialStore : IUserCredentialStore
|
||||
}
|
||||
|
||||
var descriptor = ToDescriptor(user);
|
||||
await RecordAuditAsync(
|
||||
normalized,
|
||||
descriptor.SubjectId,
|
||||
success: true,
|
||||
failureCode: null,
|
||||
reason: descriptor.RequiresPasswordReset ? "Password reset required." : null,
|
||||
auditProperties,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return AuthorityCredentialVerificationResult.Success(
|
||||
descriptor,
|
||||
descriptor.RequiresPasswordReset ? "Password reset required." : null,
|
||||
@@ -142,6 +170,15 @@ internal sealed class StandardUserCredentialStore : IUserCredentialStore
|
||||
});
|
||||
}
|
||||
|
||||
await RecordAuditAsync(
|
||||
normalized,
|
||||
user.SubjectId,
|
||||
success: false,
|
||||
failureCode: code,
|
||||
reason: code == AuthorityCredentialFailureCode.LockedOut ? "Account is temporarily locked." : "Invalid credentials.",
|
||||
auditProperties,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return AuthorityCredentialVerificationResult.Failure(
|
||||
code,
|
||||
code == AuthorityCredentialFailureCode.LockedOut ? "Account is temporarily locked." : "Invalid credentials.",
|
||||
@@ -371,4 +408,24 @@ internal sealed class StandardUserCredentialStore : IUserCredentialStore
|
||||
logger.LogDebug("Plugin {PluginName} skipped index creation due to existing index.", pluginName);
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask RecordAuditAsync(
|
||||
string normalizedUsername,
|
||||
string? subjectId,
|
||||
bool success,
|
||||
AuthorityCredentialFailureCode? failureCode,
|
||||
string? reason,
|
||||
IReadOnlyList<AuthEventProperty> auditProperties,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await auditLogger.RecordAsync(
|
||||
pluginName,
|
||||
normalizedUsername,
|
||||
subjectId,
|
||||
success,
|
||||
failureCode,
|
||||
reason,
|
||||
auditProperties,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
| SEC2.PLG | BLOCKED (2025-10-21) | Security Guild, Storage Guild | SEC2.A (audit contract) | Emit audit events from password verification outcomes and persist via `IAuthorityLoginAttemptStore`. <br>⛔ Waiting on AUTH-DPOP-11-001 / AUTH-MTLS-11-002 to stabilise Authority auth surfaces (PLUGIN-DI-08-001 landed 2025-10-21). | ✅ Serilog events enriched with subject/client/IP/outcome; ✅ Mongo records written per attempt; ✅ Tests assert success/lockout/failure cases. |
|
||||
| SEC2.PLG | DOING (2025-11-08) | Security Guild, Storage Guild | SEC2.A (audit contract) | Emit audit events from password verification outcomes and persist via `IAuthorityLoginAttemptStore`. | ✅ Serilog events enriched with subject/client/IP/outcome; ✅ Mongo records written per attempt; ✅ Tests assert success/lockout/failure cases. |
|
||||
| SEC3.PLG | BLOCKED (2025-10-21) | Security Guild, BE-Auth Plugin | CORE8, SEC3.A (rate limiter) | Ensure lockout responses and rate-limit metadata flow through plugin logs/events (include retry-after). <br>⛔ Pending AUTH-DPOP-11-001 / AUTH-MTLS-11-002; PLUGIN-DI-08-001 is done, limiter telemetry just awaits the updated Authority surface. | ✅ Audit record includes retry-after; ✅ Tests confirm lockout + limiter interplay. |
|
||||
| SEC5.PLG | BLOCKED (2025-10-21) | Security Guild | SEC5.A (threat model) | Address plugin-specific mitigations (bootstrap user handling, password policy docs) in threat model backlog. <br>⛔ Final documentation now hinges on AUTH-DPOP-11-001 / AUTH-MTLS-11-002; scoped DI work is complete. | ✅ Threat model lists plugin attack surfaces; ✅ Mitigation items filed. |
|
||||
| PLG4-6.CAPABILITIES | BLOCKED (2025-10-12) | BE-Auth Plugin, Docs Guild | PLG1–PLG3 | Finalise capability metadata exposure, config validation, and developer guide updates; remaining action is Docs polish/diagram export. | ✅ Capability metadata + validation merged; ✅ Plugin guide updated with final copy & diagrams; ✅ Release notes mention new toggles. <br>⛔ Blocked awaiting Authority rate-limiter stream (CORE8/SEC3) to resume so doc updates reflect final limiter behaviour. |
|
||||
| PLG4-6.CAPABILITIES | DONE (2025-11-08) | BE-Auth Plugin, Docs Guild | PLG1–PLG3 | Finalise capability metadata exposure, config validation, and developer guide updates; remaining action is Docs polish/diagram export. | ✅ Capability metadata + validation merged; ✅ Plugin guide updated with final copy & diagrams; ✅ Release notes mention new toggles. |
|
||||
| PLG7.RFC | DONE (2025-11-03) | BE-Auth Plugin, Security Guild | PLG4 | Socialize LDAP plugin RFC (`docs/rfcs/authority-plugin-ldap.md`) and capture guild feedback. | ✅ Guild review sign-off recorded; ✅ Follow-up issues filed in module boards. |
|
||||
| PLG7.IMPL-001 | DONE (2025-11-03) | BE-Auth Plugin | PLG7.RFC | Scaffold `StellaOps.Authority.Plugin.Ldap` + tests, bind configuration (client certificate, trust-store, insecure toggle) with validation and docs samples. | ✅ Project + test harness build; ✅ Configuration bound & validated; ✅ Sample config updated. |
|
||||
| PLG7.IMPL-002 | DONE (2025-11-04) | BE-Auth Plugin, Security Guild | PLG7.IMPL-001 | Implement LDAP credential store with TLS/mutual TLS enforcement, deterministic retry/backoff, and structured logging/metrics. | ✅ Credential store passes integration tests (OpenLDAP + mtls); ✅ Metrics/logs emitted; ✅ Error mapping documented.<br>2025-11-04: DirectoryServices factory now enforces TLS/mTLS options, credential store retries use deterministic backoff with metrics, audit logging includes failure codes, and unit suite (`dotnet test src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Ldap.Tests`) remains green. |
|
||||
@@ -19,6 +19,8 @@
|
||||
> 2025-11-03: PLG7.IMPL-001 completed – created `StellaOps.Authority.Plugin.Ldap` + tests projects, implemented configuration normalization/validation (client certificate, trust store, insecure toggle) with coverage (`dotnet test src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Ldap.Tests/StellaOps.Authority.Plugin.Ldap.Tests.csproj`), and refreshed `etc/authority.plugins/ldap.yaml`.
|
||||
> 2025-11-04: PLG7.IMPL-002 progress – StartTLS initialization now uses `StartTransportLayerSecurity(null)` and LDAP result-code handling normalized for `System.DirectoryServices.Protocols` 8.0 (invalid credentials + transient cases). Plugin tests rerun via `dotnet test src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Ldap.Tests/StellaOps.Authority.Plugin.Ldap.Tests.csproj` (green).
|
||||
> 2025-11-04: PLG7.IMPL-002 progress – enforced TLS/client certificate validation, added structured audit properties and retry logging for credential lookups, warned on unsupported cipher lists, updated sample config, and reran `dotnet test src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Ldap.Tests/StellaOps.Authority.Plugin.Ldap.Tests.csproj --no-restore`.
|
||||
> 2025-11-08: PLG4-6.CAPABILITIES completed – added the `bootstrap` capability flag, extended registries/logs/docs, and gated bootstrap APIs on the new capability (`dotnet test` suites for plugins + Authority core all green).
|
||||
> 2025-11-08: SEC2.PLG resumed – Standard plugin now records password verification outcomes via `StandardCredentialAuditLogger`, persisting events to `IAuthorityLoginAttemptStore`; unit tests cover success/lockout/failure flows (`dotnet test src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Standard.Tests/StellaOps.Authority.Plugin.Standard.Tests.csproj --no-build`).
|
||||
|
||||
> Update statuses to DOING/DONE/BLOCKED as you make progress. Always run `dotnet test` for touched projects before marking DONE.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user