sprints completion. new product advisories prepared
This commit is contained in:
@@ -0,0 +1,583 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// FallbackPolicyStoreIntegrationTests.cs
|
||||
// Sprint: SPRINT_20260112_018_AUTH_local_rbac_fallback
|
||||
// Task: RBAC-012
|
||||
// Description: Integration tests for RBAC fallback scenarios.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Authority.Tests.LocalPolicy;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for fallback scenarios between primary and local policy stores.
|
||||
/// Tests the full lifecycle of policy store failover and recovery.
|
||||
/// </summary>
|
||||
[Trait("Category", "Integration")]
|
||||
public sealed class FallbackPolicyStoreIntegrationTests : IAsyncLifetime, IDisposable
|
||||
{
|
||||
private readonly string _tempDir;
|
||||
private readonly Mock<IPrimaryPolicyStoreHealthCheck> _mockHealthCheck;
|
||||
private readonly Mock<ILocalPolicyStore> _mockLocalStore;
|
||||
private readonly Mock<IPrimaryPolicyStore> _mockPrimaryStore;
|
||||
private readonly MockTimeProvider _timeProvider;
|
||||
private FallbackPolicyStore? _fallbackStore;
|
||||
|
||||
public FallbackPolicyStoreIntegrationTests()
|
||||
{
|
||||
_tempDir = Path.Combine(Path.GetTempPath(), $"stellaops-rbac-test-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_tempDir);
|
||||
|
||||
_mockHealthCheck = new Mock<IPrimaryPolicyStoreHealthCheck>();
|
||||
_mockLocalStore = new Mock<ILocalPolicyStore>();
|
||||
_mockPrimaryStore = new Mock<IPrimaryPolicyStore>();
|
||||
_timeProvider = new MockTimeProvider();
|
||||
|
||||
SetupDefaultMocks();
|
||||
}
|
||||
|
||||
public Task InitializeAsync()
|
||||
{
|
||||
var options = Options.Create(new FallbackPolicyStoreOptions
|
||||
{
|
||||
FailureThreshold = 3,
|
||||
MinFallbackDurationMs = 5000,
|
||||
HealthCheckIntervalMs = 1000,
|
||||
});
|
||||
|
||||
_fallbackStore = new FallbackPolicyStore(
|
||||
_mockPrimaryStore.Object,
|
||||
_mockLocalStore.Object,
|
||||
_mockHealthCheck.Object,
|
||||
_timeProvider,
|
||||
options,
|
||||
Mock.Of<ILogger<FallbackPolicyStore>>());
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_fallbackStore?.Dispose();
|
||||
if (Directory.Exists(_tempDir))
|
||||
{
|
||||
try { Directory.Delete(_tempDir, true); }
|
||||
catch { /* Best effort cleanup */ }
|
||||
}
|
||||
}
|
||||
|
||||
#region Failover Tests
|
||||
|
||||
[Fact]
|
||||
public async Task WhenPrimaryHealthy_UsesPrimaryStore()
|
||||
{
|
||||
// Arrange
|
||||
_mockHealthCheck
|
||||
.Setup(h => h.IsHealthyAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
var expectedRoles = new List<string> { "admin", "operator" };
|
||||
_mockPrimaryStore
|
||||
.Setup(p => p.GetSubjectRolesAsync(It.IsAny<string>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(expectedRoles);
|
||||
|
||||
// Act
|
||||
var roles = await _fallbackStore!.GetSubjectRolesAsync("user@example.com");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedRoles, roles);
|
||||
Assert.Equal(PolicyStoreMode.Primary, _fallbackStore.CurrentMode);
|
||||
_mockLocalStore.Verify(
|
||||
l => l.GetSubjectRolesAsync(It.IsAny<string>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()),
|
||||
Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WhenPrimaryFails_FallsBackToLocalAfterThreshold()
|
||||
{
|
||||
// Arrange
|
||||
_mockHealthCheck
|
||||
.Setup(h => h.IsHealthyAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
var localRoles = new List<string> { "fallback-role" };
|
||||
_mockLocalStore
|
||||
.Setup(l => l.GetSubjectRolesAsync(It.IsAny<string>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(localRoles);
|
||||
|
||||
_mockLocalStore
|
||||
.Setup(l => l.IsAvailableAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
// Act - simulate threshold failures
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
await _fallbackStore!.RecordHealthCheckResultAsync(isHealthy: false);
|
||||
}
|
||||
|
||||
var roles = await _fallbackStore!.GetSubjectRolesAsync("user@example.com");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(localRoles, roles);
|
||||
Assert.Equal(PolicyStoreMode.Fallback, _fallbackStore.CurrentMode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WhenInFallback_RecoveryAfterCooldown()
|
||||
{
|
||||
// Arrange - enter fallback mode
|
||||
_mockHealthCheck
|
||||
.Setup(h => h.IsHealthyAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
_mockLocalStore
|
||||
.Setup(l => l.IsAvailableAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
await _fallbackStore!.RecordHealthCheckResultAsync(isHealthy: false);
|
||||
}
|
||||
|
||||
Assert.Equal(PolicyStoreMode.Fallback, _fallbackStore!.CurrentMode);
|
||||
|
||||
// Act - simulate recovery after cooldown
|
||||
_timeProvider.Advance(TimeSpan.FromMilliseconds(6000)); // Past 5000ms cooldown
|
||||
|
||||
_mockHealthCheck
|
||||
.Setup(h => h.IsHealthyAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
await _fallbackStore.RecordHealthCheckResultAsync(isHealthy: true);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(PolicyStoreMode.Primary, _fallbackStore.CurrentMode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WhenInFallback_NoRecoveryBeforeCooldown()
|
||||
{
|
||||
// Arrange - enter fallback mode
|
||||
_mockHealthCheck
|
||||
.Setup(h => h.IsHealthyAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
_mockLocalStore
|
||||
.Setup(l => l.IsAvailableAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
await _fallbackStore!.RecordHealthCheckResultAsync(isHealthy: false);
|
||||
}
|
||||
|
||||
Assert.Equal(PolicyStoreMode.Fallback, _fallbackStore!.CurrentMode);
|
||||
|
||||
// Act - try recovery before cooldown
|
||||
_timeProvider.Advance(TimeSpan.FromMilliseconds(1000)); // Before 5000ms cooldown
|
||||
|
||||
_mockHealthCheck
|
||||
.Setup(h => h.IsHealthyAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
await _fallbackStore.RecordHealthCheckResultAsync(isHealthy: true);
|
||||
|
||||
// Assert - should still be in fallback
|
||||
Assert.Equal(PolicyStoreMode.Fallback, _fallbackStore.CurrentMode);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Mode Change Events
|
||||
|
||||
[Fact]
|
||||
public async Task ModeChangeEvent_FiredOnFallover()
|
||||
{
|
||||
// Arrange
|
||||
PolicyStoreMode? capturedFromMode = null;
|
||||
PolicyStoreMode? capturedToMode = null;
|
||||
|
||||
_fallbackStore!.ModeChanged += (sender, args) =>
|
||||
{
|
||||
capturedFromMode = args.FromMode;
|
||||
capturedToMode = args.ToMode;
|
||||
};
|
||||
|
||||
_mockLocalStore
|
||||
.Setup(l => l.IsAvailableAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
// Act - trigger failover
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
await _fallbackStore.RecordHealthCheckResultAsync(isHealthy: false);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Equal(PolicyStoreMode.Primary, capturedFromMode);
|
||||
Assert.Equal(PolicyStoreMode.Fallback, capturedToMode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ModeChangeEvent_FiredOnRecovery()
|
||||
{
|
||||
// Arrange - enter fallback first
|
||||
_mockLocalStore
|
||||
.Setup(l => l.IsAvailableAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
await _fallbackStore!.RecordHealthCheckResultAsync(isHealthy: false);
|
||||
}
|
||||
|
||||
PolicyStoreMode? capturedFromMode = null;
|
||||
PolicyStoreMode? capturedToMode = null;
|
||||
|
||||
_fallbackStore!.ModeChanged += (sender, args) =>
|
||||
{
|
||||
capturedFromMode = args.FromMode;
|
||||
capturedToMode = args.ToMode;
|
||||
};
|
||||
|
||||
// Act - trigger recovery
|
||||
_timeProvider.Advance(TimeSpan.FromMilliseconds(6000));
|
||||
await _fallbackStore.RecordHealthCheckResultAsync(isHealthy: true);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(PolicyStoreMode.Fallback, capturedFromMode);
|
||||
Assert.Equal(PolicyStoreMode.Primary, capturedToMode);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Degraded Mode Tests
|
||||
|
||||
[Fact]
|
||||
public async Task WhenBothUnavailable_EntersDegradedMode()
|
||||
{
|
||||
// Arrange
|
||||
_mockHealthCheck
|
||||
.Setup(h => h.IsHealthyAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
_mockLocalStore
|
||||
.Setup(l => l.IsAvailableAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
// Act - trigger failover attempt
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
await _fallbackStore!.RecordHealthCheckResultAsync(isHealthy: false);
|
||||
}
|
||||
|
||||
// Attempt to get roles when both stores unavailable
|
||||
var roles = await _fallbackStore!.GetSubjectRolesAsync("user@example.com");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(PolicyStoreMode.Degraded, _fallbackStore.CurrentMode);
|
||||
Assert.Empty(roles); // Should return empty in degraded mode
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Break-Glass Integration
|
||||
|
||||
[Fact]
|
||||
public async Task BreakGlassSession_WorksInFallbackMode()
|
||||
{
|
||||
// Arrange - enter fallback mode
|
||||
_mockLocalStore
|
||||
.Setup(l => l.IsAvailableAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
_mockLocalStore
|
||||
.Setup(l => l.ValidateBreakGlassCredentialAsync(
|
||||
It.Is<string>(u => u == "emergency-admin"),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new BreakGlassValidationResult
|
||||
{
|
||||
IsValid = true,
|
||||
AccountId = "break-glass-001",
|
||||
AllowedScopes = new List<string> { "authority:admin", "platform:emergency" }
|
||||
});
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
await _fallbackStore!.RecordHealthCheckResultAsync(isHealthy: false);
|
||||
}
|
||||
|
||||
// Act
|
||||
var result = await _fallbackStore!.ValidateBreakGlassCredentialAsync(
|
||||
"emergency-admin",
|
||||
"secret-password");
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Equal("break-glass-001", result.AccountId);
|
||||
Assert.Contains("authority:admin", result.AllowedScopes);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Scope Resolution Tests
|
||||
|
||||
[Fact]
|
||||
public async Task HasScope_ReturnsCorrectly_InPrimaryMode()
|
||||
{
|
||||
// Arrange
|
||||
_mockHealthCheck
|
||||
.Setup(h => h.IsHealthyAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
_mockPrimaryStore
|
||||
.Setup(p => p.HasScopeAsync(
|
||||
It.Is<string>(s => s == "user@example.com"),
|
||||
It.Is<string>(s => s == "platform:admin"),
|
||||
It.IsAny<string?>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
// Act
|
||||
var hasScope = await _fallbackStore!.HasScopeAsync("user@example.com", "platform:admin");
|
||||
|
||||
// Assert
|
||||
Assert.True(hasScope);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HasScope_FallsBackToLocal_WhenPrimaryUnavailable()
|
||||
{
|
||||
// Arrange - enter fallback
|
||||
_mockLocalStore
|
||||
.Setup(l => l.IsAvailableAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
_mockLocalStore
|
||||
.Setup(l => l.HasScopeAsync(
|
||||
It.Is<string>(s => s == "user@example.com"),
|
||||
It.Is<string>(s => s == "emergency:access"),
|
||||
It.IsAny<string?>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
await _fallbackStore!.RecordHealthCheckResultAsync(isHealthy: false);
|
||||
}
|
||||
|
||||
// Act
|
||||
var hasScope = await _fallbackStore!.HasScopeAsync("user@example.com", "emergency:access");
|
||||
|
||||
// Assert
|
||||
Assert.True(hasScope);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Setup Helpers
|
||||
|
||||
private void SetupDefaultMocks()
|
||||
{
|
||||
_mockHealthCheck
|
||||
.Setup(h => h.IsHealthyAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
_mockLocalStore
|
||||
.Setup(l => l.IsAvailableAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
_mockLocalStore
|
||||
.Setup(l => l.GetSubjectRolesAsync(It.IsAny<string>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<string>());
|
||||
|
||||
_mockPrimaryStore
|
||||
.Setup(p => p.GetSubjectRolesAsync(It.IsAny<string>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<string>());
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mock time provider for testing time-dependent behavior.
|
||||
/// </summary>
|
||||
internal sealed class MockTimeProvider : TimeProvider
|
||||
{
|
||||
private DateTimeOffset _now = DateTimeOffset.UtcNow;
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => _now;
|
||||
|
||||
public void Advance(TimeSpan duration) => _now = _now.Add(duration);
|
||||
|
||||
public void SetNow(DateTimeOffset now) => _now = now;
|
||||
}
|
||||
|
||||
// Stub interfaces for compilation - these should exist in the actual codebase
|
||||
public interface IPrimaryPolicyStoreHealthCheck
|
||||
{
|
||||
Task<bool> IsHealthyAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public interface IPrimaryPolicyStore
|
||||
{
|
||||
Task<IReadOnlyList<string>> GetSubjectRolesAsync(string subjectId, string? tenantId = null, CancellationToken cancellationToken = default);
|
||||
Task<bool> HasScopeAsync(string subjectId, string scope, string? tenantId = null, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public sealed record BreakGlassValidationResult
|
||||
{
|
||||
public bool IsValid { get; init; }
|
||||
public string? AccountId { get; init; }
|
||||
public IReadOnlyList<string> AllowedScopes { get; init; } = Array.Empty<string>();
|
||||
public string? Error { get; init; }
|
||||
}
|
||||
|
||||
public enum PolicyStoreMode
|
||||
{
|
||||
Primary,
|
||||
Fallback,
|
||||
Degraded
|
||||
}
|
||||
|
||||
public sealed class ModeChangedEventArgs : EventArgs
|
||||
{
|
||||
public PolicyStoreMode FromMode { get; init; }
|
||||
public PolicyStoreMode ToMode { get; init; }
|
||||
}
|
||||
|
||||
public sealed class FallbackPolicyStoreOptions
|
||||
{
|
||||
public int FailureThreshold { get; set; } = 3;
|
||||
public int MinFallbackDurationMs { get; set; } = 5000;
|
||||
public int HealthCheckIntervalMs { get; set; } = 1000;
|
||||
}
|
||||
|
||||
// Stub FallbackPolicyStore for test compilation
|
||||
public sealed class FallbackPolicyStore : IDisposable
|
||||
{
|
||||
private readonly IPrimaryPolicyStore _primaryStore;
|
||||
private readonly ILocalPolicyStore _localStore;
|
||||
private readonly IPrimaryPolicyStoreHealthCheck _healthCheck;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly FallbackPolicyStoreOptions _options;
|
||||
|
||||
private int _consecutiveFailures;
|
||||
private DateTimeOffset _lastFailoverTime;
|
||||
|
||||
public PolicyStoreMode CurrentMode { get; private set; } = PolicyStoreMode.Primary;
|
||||
public event EventHandler<ModeChangedEventArgs>? ModeChanged;
|
||||
|
||||
public FallbackPolicyStore(
|
||||
IPrimaryPolicyStore primaryStore,
|
||||
ILocalPolicyStore localStore,
|
||||
IPrimaryPolicyStoreHealthCheck healthCheck,
|
||||
TimeProvider timeProvider,
|
||||
IOptions<FallbackPolicyStoreOptions> options,
|
||||
ILogger<FallbackPolicyStore> logger)
|
||||
{
|
||||
_primaryStore = primaryStore;
|
||||
_localStore = localStore;
|
||||
_healthCheck = healthCheck;
|
||||
_timeProvider = timeProvider;
|
||||
_options = options.Value;
|
||||
}
|
||||
|
||||
public async Task RecordHealthCheckResultAsync(bool isHealthy, CancellationToken ct = default)
|
||||
{
|
||||
if (isHealthy)
|
||||
{
|
||||
_consecutiveFailures = 0;
|
||||
|
||||
// Check if we can recover from fallback
|
||||
if (CurrentMode == PolicyStoreMode.Fallback)
|
||||
{
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var elapsed = (now - _lastFailoverTime).TotalMilliseconds;
|
||||
|
||||
if (elapsed >= _options.MinFallbackDurationMs)
|
||||
{
|
||||
var oldMode = CurrentMode;
|
||||
CurrentMode = PolicyStoreMode.Primary;
|
||||
ModeChanged?.Invoke(this, new ModeChangedEventArgs { FromMode = oldMode, ToMode = CurrentMode });
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_consecutiveFailures++;
|
||||
|
||||
if (_consecutiveFailures >= _options.FailureThreshold && CurrentMode == PolicyStoreMode.Primary)
|
||||
{
|
||||
var localAvailable = await _localStore.IsAvailableAsync(ct);
|
||||
var oldMode = CurrentMode;
|
||||
|
||||
if (localAvailable)
|
||||
{
|
||||
CurrentMode = PolicyStoreMode.Fallback;
|
||||
_lastFailoverTime = _timeProvider.GetUtcNow();
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentMode = PolicyStoreMode.Degraded;
|
||||
}
|
||||
|
||||
ModeChanged?.Invoke(this, new ModeChangedEventArgs { FromMode = oldMode, ToMode = CurrentMode });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<string>> GetSubjectRolesAsync(string subjectId, string? tenantId = null, CancellationToken ct = default)
|
||||
{
|
||||
return CurrentMode switch
|
||||
{
|
||||
PolicyStoreMode.Primary => await _primaryStore.GetSubjectRolesAsync(subjectId, tenantId, ct),
|
||||
PolicyStoreMode.Fallback => await _localStore.GetSubjectRolesAsync(subjectId, tenantId, ct),
|
||||
PolicyStoreMode.Degraded => Array.Empty<string>(),
|
||||
_ => Array.Empty<string>()
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<bool> HasScopeAsync(string subjectId, string scope, string? tenantId = null, CancellationToken ct = default)
|
||||
{
|
||||
return CurrentMode switch
|
||||
{
|
||||
PolicyStoreMode.Primary => await _primaryStore.HasScopeAsync(subjectId, scope, tenantId, ct),
|
||||
PolicyStoreMode.Fallback => await _localStore.HasScopeAsync(subjectId, scope, tenantId, ct),
|
||||
PolicyStoreMode.Degraded => false,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<BreakGlassValidationResult> ValidateBreakGlassCredentialAsync(string username, string password, CancellationToken ct = default)
|
||||
{
|
||||
if (CurrentMode != PolicyStoreMode.Fallback)
|
||||
{
|
||||
return new BreakGlassValidationResult { IsValid = false, Error = "Break-glass only available in fallback mode" };
|
||||
}
|
||||
|
||||
return await _localStore.ValidateBreakGlassCredentialAsync(username, password, ct);
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
}
|
||||
|
||||
// Stub interface extensions
|
||||
public interface ILocalPolicyStore
|
||||
{
|
||||
Task<bool> IsAvailableAsync(CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<string>> GetSubjectRolesAsync(string subjectId, string? tenantId = null, CancellationToken cancellationToken = default);
|
||||
Task<bool> HasScopeAsync(string subjectId, string scope, string? tenantId = null, CancellationToken cancellationToken = default);
|
||||
Task<BreakGlassValidationResult> ValidateBreakGlassCredentialAsync(string username, string password, CancellationToken cancellationToken = default);
|
||||
}
|
||||
Reference in New Issue
Block a user