|
|
|
|
@@ -15,7 +15,174 @@ using Microsoft.Extensions.Options;
|
|
|
|
|
using Moq;
|
|
|
|
|
using Xunit;
|
|
|
|
|
|
|
|
|
|
namespace StellaOps.Authority.Tests.LocalPolicy;
|
|
|
|
|
namespace StellaOps.Authority.Tests.LocalPolicy
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
// Stub interfaces for compilation - these should exist in the actual codebase
|
|
|
|
|
internal interface ITestPrimaryPolicyStoreHealthCheck
|
|
|
|
|
{
|
|
|
|
|
Task<bool> IsHealthyAsync(CancellationToken cancellationToken = default);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal interface ITestPrimaryPolicyStore
|
|
|
|
|
{
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal sealed record TestBreakGlassValidationResult
|
|
|
|
|
{
|
|
|
|
|
public bool IsValid { get; init; }
|
|
|
|
|
public string? AccountId { get; init; }
|
|
|
|
|
public IReadOnlyList<string> AllowedScopes { get; init; } = Array.Empty<string>();
|
|
|
|
|
public string? Error { get; init; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal enum TestPolicyStoreMode
|
|
|
|
|
{
|
|
|
|
|
Primary,
|
|
|
|
|
Fallback,
|
|
|
|
|
Degraded
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal sealed class TestModeChangedEventArgs : EventArgs
|
|
|
|
|
{
|
|
|
|
|
public TestPolicyStoreMode FromMode { get; init; }
|
|
|
|
|
public TestPolicyStoreMode ToMode { get; init; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal sealed class TestFallbackPolicyStoreOptions
|
|
|
|
|
{
|
|
|
|
|
public int FailureThreshold { get; set; } = 3;
|
|
|
|
|
public int MinFallbackDurationMs { get; set; } = 5000;
|
|
|
|
|
public int HealthCheckIntervalMs { get; set; } = 1000;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal interface ITestLocalPolicyStore
|
|
|
|
|
{
|
|
|
|
|
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<TestBreakGlassValidationResult> ValidateBreakGlassCredentialAsync(string username, string password, CancellationToken cancellationToken = default);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal sealed class TestFallbackPolicyStore : IDisposable
|
|
|
|
|
{
|
|
|
|
|
private readonly ITestPrimaryPolicyStore _primaryStore;
|
|
|
|
|
private readonly ITestLocalPolicyStore _localStore;
|
|
|
|
|
private readonly ITestPrimaryPolicyStoreHealthCheck _healthCheck;
|
|
|
|
|
private readonly TimeProvider _timeProvider;
|
|
|
|
|
private readonly TestFallbackPolicyStoreOptions _options;
|
|
|
|
|
|
|
|
|
|
private int _consecutiveFailures;
|
|
|
|
|
private DateTimeOffset _lastFailoverTime;
|
|
|
|
|
|
|
|
|
|
public TestPolicyStoreMode CurrentMode { get; private set; } = TestPolicyStoreMode.Primary;
|
|
|
|
|
public event EventHandler<TestModeChangedEventArgs>? ModeChanged;
|
|
|
|
|
|
|
|
|
|
public TestFallbackPolicyStore(
|
|
|
|
|
ITestPrimaryPolicyStore primaryStore,
|
|
|
|
|
ITestLocalPolicyStore localStore,
|
|
|
|
|
ITestPrimaryPolicyStoreHealthCheck healthCheck,
|
|
|
|
|
TimeProvider timeProvider,
|
|
|
|
|
IOptions<TestFallbackPolicyStoreOptions> options,
|
|
|
|
|
ILogger<TestFallbackPolicyStore> 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;
|
|
|
|
|
|
|
|
|
|
if (CurrentMode == TestPolicyStoreMode.Fallback)
|
|
|
|
|
{
|
|
|
|
|
var now = _timeProvider.GetUtcNow();
|
|
|
|
|
var elapsed = (now - _lastFailoverTime).TotalMilliseconds;
|
|
|
|
|
|
|
|
|
|
if (elapsed >= _options.MinFallbackDurationMs)
|
|
|
|
|
{
|
|
|
|
|
var oldMode = CurrentMode;
|
|
|
|
|
CurrentMode = TestPolicyStoreMode.Primary;
|
|
|
|
|
ModeChanged?.Invoke(this, new TestModeChangedEventArgs { FromMode = oldMode, ToMode = CurrentMode });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
_consecutiveFailures++;
|
|
|
|
|
|
|
|
|
|
if (_consecutiveFailures >= _options.FailureThreshold && CurrentMode == TestPolicyStoreMode.Primary)
|
|
|
|
|
{
|
|
|
|
|
var localAvailable = await _localStore.IsAvailableAsync(ct);
|
|
|
|
|
var oldMode = CurrentMode;
|
|
|
|
|
|
|
|
|
|
if (localAvailable)
|
|
|
|
|
{
|
|
|
|
|
CurrentMode = TestPolicyStoreMode.Fallback;
|
|
|
|
|
_lastFailoverTime = _timeProvider.GetUtcNow();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
CurrentMode = TestPolicyStoreMode.Degraded;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ModeChanged?.Invoke(this, new TestModeChangedEventArgs { FromMode = oldMode, ToMode = CurrentMode });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<IReadOnlyList<string>> GetSubjectRolesAsync(string subjectId, string? tenantId = null, CancellationToken ct = default)
|
|
|
|
|
{
|
|
|
|
|
return CurrentMode switch
|
|
|
|
|
{
|
|
|
|
|
TestPolicyStoreMode.Primary => await _primaryStore.GetSubjectRolesAsync(subjectId, tenantId, ct),
|
|
|
|
|
TestPolicyStoreMode.Fallback => await _localStore.GetSubjectRolesAsync(subjectId, tenantId, ct),
|
|
|
|
|
TestPolicyStoreMode.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
|
|
|
|
|
{
|
|
|
|
|
TestPolicyStoreMode.Primary => await _primaryStore.HasScopeAsync(subjectId, scope, tenantId, ct),
|
|
|
|
|
TestPolicyStoreMode.Fallback => await _localStore.HasScopeAsync(subjectId, scope, tenantId, ct),
|
|
|
|
|
TestPolicyStoreMode.Degraded => false,
|
|
|
|
|
_ => false
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<TestBreakGlassValidationResult> ValidateBreakGlassCredentialAsync(string username, string password, CancellationToken ct = default)
|
|
|
|
|
{
|
|
|
|
|
if (CurrentMode != TestPolicyStoreMode.Fallback)
|
|
|
|
|
{
|
|
|
|
|
return new TestBreakGlassValidationResult { IsValid = false, Error = "Break-glass only available in fallback mode" };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return await _localStore.ValidateBreakGlassCredentialAsync(username, password, ct);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Dispose() { }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Integration tests for fallback scenarios between primary and local policy stores.
|
|
|
|
|
@@ -44,7 +211,7 @@ public sealed class FallbackPolicyStoreIntegrationTests : IAsyncLifetime, IDispo
|
|
|
|
|
SetupDefaultMocks();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Task InitializeAsync()
|
|
|
|
|
public ValueTask InitializeAsync()
|
|
|
|
|
{
|
|
|
|
|
var options = Options.Create(new FallbackPolicyStoreOptions
|
|
|
|
|
{
|
|
|
|
|
@@ -61,10 +228,10 @@ public sealed class FallbackPolicyStoreIntegrationTests : IAsyncLifetime, IDispo
|
|
|
|
|
options,
|
|
|
|
|
Mock.Of<ILogger<FallbackPolicyStore>>());
|
|
|
|
|
|
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
return ValueTask.CompletedTask;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Task DisposeAsync() => Task.CompletedTask;
|
|
|
|
|
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
|
|
|
|
|
|
|
|
|
public void Dispose()
|
|
|
|
|
{
|
|
|
|
|
@@ -424,160 +591,164 @@ internal sealed class MockTimeProvider : TimeProvider
|
|
|
|
|
public void SetNow(DateTimeOffset now) => _now = now;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Stub interfaces for compilation - these should exist in the actual codebase
|
|
|
|
|
public interface IPrimaryPolicyStoreHealthCheck
|
|
|
|
|
namespace StellaOps.Authority.Tests.LocalPolicy.Stubs
|
|
|
|
|
{
|
|
|
|
|
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)
|
|
|
|
|
// Stub interfaces for compilation - these should exist in the actual codebase
|
|
|
|
|
public interface IPrimaryPolicyStoreHealthCheck
|
|
|
|
|
{
|
|
|
|
|
_primaryStore = primaryStore;
|
|
|
|
|
_localStore = localStore;
|
|
|
|
|
_healthCheck = healthCheck;
|
|
|
|
|
_timeProvider = timeProvider;
|
|
|
|
|
_options = options.Value;
|
|
|
|
|
Task<bool> IsHealthyAsync(CancellationToken cancellationToken = default);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task RecordHealthCheckResultAsync(bool isHealthy, CancellationToken ct = default)
|
|
|
|
|
public interface IPrimaryPolicyStore
|
|
|
|
|
{
|
|
|
|
|
if (isHealthy)
|
|
|
|
|
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)
|
|
|
|
|
{
|
|
|
|
|
_consecutiveFailures = 0;
|
|
|
|
|
_primaryStore = primaryStore;
|
|
|
|
|
_localStore = localStore;
|
|
|
|
|
_healthCheck = healthCheck;
|
|
|
|
|
_timeProvider = timeProvider;
|
|
|
|
|
_options = options.Value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if we can recover from fallback
|
|
|
|
|
if (CurrentMode == PolicyStoreMode.Fallback)
|
|
|
|
|
public async Task RecordHealthCheckResultAsync(bool isHealthy, CancellationToken ct = default)
|
|
|
|
|
{
|
|
|
|
|
if (isHealthy)
|
|
|
|
|
{
|
|
|
|
|
var now = _timeProvider.GetUtcNow();
|
|
|
|
|
var elapsed = (now - _lastFailoverTime).TotalMilliseconds;
|
|
|
|
|
_consecutiveFailures = 0;
|
|
|
|
|
|
|
|
|
|
if (elapsed >= _options.MinFallbackDurationMs)
|
|
|
|
|
// 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;
|
|
|
|
|
CurrentMode = PolicyStoreMode.Primary;
|
|
|
|
|
|
|
|
|
|
if (localAvailable)
|
|
|
|
|
{
|
|
|
|
|
CurrentMode = PolicyStoreMode.Fallback;
|
|
|
|
|
_lastFailoverTime = _timeProvider.GetUtcNow();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
CurrentMode = PolicyStoreMode.Degraded;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ModeChanged?.Invoke(this, new ModeChangedEventArgs { FromMode = oldMode, ToMode = CurrentMode });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
_consecutiveFailures++;
|
|
|
|
|
|
|
|
|
|
if (_consecutiveFailures >= _options.FailureThreshold && CurrentMode == PolicyStoreMode.Primary)
|
|
|
|
|
public async Task<IReadOnlyList<string>> GetSubjectRolesAsync(string subjectId, string? tenantId = null, CancellationToken ct = default)
|
|
|
|
|
{
|
|
|
|
|
return CurrentMode switch
|
|
|
|
|
{
|
|
|
|
|
var localAvailable = await _localStore.IsAvailableAsync(ct);
|
|
|
|
|
var oldMode = CurrentMode;
|
|
|
|
|
PolicyStoreMode.Primary => await _primaryStore.GetSubjectRolesAsync(subjectId, tenantId, ct),
|
|
|
|
|
PolicyStoreMode.Fallback => await _localStore.GetSubjectRolesAsync(subjectId, tenantId, ct),
|
|
|
|
|
PolicyStoreMode.Degraded => Array.Empty<string>(),
|
|
|
|
|
_ => Array.Empty<string>()
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (localAvailable)
|
|
|
|
|
{
|
|
|
|
|
CurrentMode = PolicyStoreMode.Fallback;
|
|
|
|
|
_lastFailoverTime = _timeProvider.GetUtcNow();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
CurrentMode = PolicyStoreMode.Degraded;
|
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ModeChanged?.Invoke(this, new ModeChangedEventArgs { FromMode = oldMode, ToMode = CurrentMode });
|
|
|
|
|
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" };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return await _localStore.ValidateBreakGlassCredentialAsync(username, password, ct);
|
|
|
|
|
public void Dispose() { }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
|