save progress

This commit is contained in:
StellaOps Bot
2026-01-06 09:42:02 +02:00
parent 94d68bee8b
commit 37e11918e0
443 changed files with 85863 additions and 897 deletions

View File

@@ -25,7 +25,11 @@ internal sealed class LdapIdentityProviderPlugin : IIdentityProviderPlugin
private readonly LdapCapabilityProbe capabilityProbe;
private readonly AuthorityIdentityProviderCapabilities manifestCapabilities;
private readonly SemaphoreSlim capabilityGate = new(1, 1);
private AuthorityIdentityProviderCapabilities capabilities;
private AuthorityIdentityProviderCapabilities capabilities = new(
SupportsPassword: false,
SupportsMfa: false,
SupportsClientProvisioning: false,
SupportsBootstrap: false);
private bool clientProvisioningActive;
private bool bootstrapActive;
private bool loggedProvisioningDegrade;

View File

@@ -0,0 +1,256 @@
// <copyright file="AuthorityConfigDiffTests.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
// </copyright>
// Sprint: SPRINT_20260105_002_005_TEST_cross_cutting
// Task: CCUT-021
using System.Collections.Immutable;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.TestKit;
using StellaOps.Testing.ConfigDiff;
using Xunit;
namespace StellaOps.Authority.ConfigDiff.Tests;
/// <summary>
/// Config-diff tests for the Authority module.
/// Verifies that configuration changes produce only expected behavioral deltas.
/// </summary>
[Trait("Category", TestCategories.ConfigDiff)]
[Trait("Category", TestCategories.Integration)]
[Trait("BlastRadius", TestCategories.BlastRadius.Auth)]
public class AuthorityConfigDiffTests : ConfigDiffTestBase
{
/// <summary>
/// Initializes a new instance of the <see cref="AuthorityConfigDiffTests"/> class.
/// </summary>
public AuthorityConfigDiffTests()
: base(
new ConfigDiffTestConfig(StrictMode: true),
NullLogger.Instance)
{
}
/// <summary>
/// Verifies that changing token lifetime only affects token behavior.
/// </summary>
[Fact]
public async Task ChangingTokenLifetime_OnlyAffectsTokenBehavior()
{
// Arrange
var baselineConfig = new AuthorityTestConfig
{
AccessTokenLifetimeMinutes = 15,
RefreshTokenLifetimeHours = 24,
MaxConcurrentSessions = 5
};
var changedConfig = baselineConfig with
{
AccessTokenLifetimeMinutes = 30
};
// Act
var result = await TestConfigIsolationAsync(
baselineConfig,
changedConfig,
changedSetting: "AccessTokenLifetimeMinutes",
unrelatedBehaviors:
[
async config => await GetSessionBehaviorAsync(config),
async config => await GetRefreshBehaviorAsync(config),
async config => await GetAuthenticationBehaviorAsync(config)
]);
// Assert
result.IsSuccess.Should().BeTrue(
because: "changing token lifetime should not affect sessions or authentication");
}
/// <summary>
/// Verifies that changing max sessions produces expected behavioral delta.
/// </summary>
[Fact]
public async Task ChangingMaxSessions_ProducesExpectedDelta()
{
// Arrange
var baselineConfig = new AuthorityTestConfig { MaxConcurrentSessions = 3 };
var changedConfig = new AuthorityTestConfig { MaxConcurrentSessions = 10 };
var expectedDelta = new ConfigDelta(
ChangedBehaviors: ["SessionLimit", "ConcurrencyPolicy"],
BehaviorDeltas:
[
new BehaviorDelta("SessionLimit", "3", "10", null),
new BehaviorDelta("ConcurrencyPolicy", "restrictive", "permissive",
"More sessions allowed")
]);
// Act
var result = await TestConfigBehavioralDeltaAsync(
baselineConfig,
changedConfig,
getBehavior: async config => await CaptureSessionBehaviorAsync(config),
computeDelta: ComputeBehaviorSnapshotDelta,
expectedDelta: expectedDelta);
// Assert
result.IsSuccess.Should().BeTrue(
because: "session limit change should produce expected behavioral delta");
}
/// <summary>
/// Verifies that enabling DPoP only affects token binding.
/// </summary>
[Fact]
public async Task EnablingDPoP_OnlyAffectsTokenBinding()
{
// Arrange
var baselineConfig = new AuthorityTestConfig { EnableDPoP = false };
var changedConfig = new AuthorityTestConfig { EnableDPoP = true };
// Act
var result = await TestConfigIsolationAsync(
baselineConfig,
changedConfig,
changedSetting: "EnableDPoP",
unrelatedBehaviors:
[
async config => await GetSessionBehaviorAsync(config),
async config => await GetPasswordPolicyBehaviorAsync(config)
]);
// Assert
result.IsSuccess.Should().BeTrue(
because: "DPoP should not affect sessions or password policy");
}
/// <summary>
/// Verifies that changing password policy produces expected changes.
/// </summary>
[Fact]
public async Task ChangingPasswordMinLength_ProducesExpectedDelta()
{
// Arrange
var baselineConfig = new AuthorityTestConfig { MinPasswordLength = 8 };
var changedConfig = new AuthorityTestConfig { MinPasswordLength = 12 };
var expectedDelta = new ConfigDelta(
ChangedBehaviors: ["PasswordComplexity", "ValidationRejectionRate"],
BehaviorDeltas:
[
new BehaviorDelta("PasswordComplexity", "standard", "enhanced", null),
new BehaviorDelta("ValidationRejectionRate", "increase", null,
"Stricter requirements reject more passwords")
]);
// Act
var result = await TestConfigBehavioralDeltaAsync(
baselineConfig,
changedConfig,
getBehavior: async config => await CapturePasswordPolicyBehaviorAsync(config),
computeDelta: ComputeBehaviorSnapshotDelta,
expectedDelta: expectedDelta);
// Assert
result.IsSuccess.Should().BeTrue();
}
/// <summary>
/// Verifies that enabling MFA only affects authentication flow.
/// </summary>
[Fact]
public async Task EnablingMFA_OnlyAffectsAuthentication()
{
// Arrange
var baselineConfig = new AuthorityTestConfig { RequireMFA = false };
var changedConfig = new AuthorityTestConfig { RequireMFA = true };
// Act
var result = await TestConfigIsolationAsync(
baselineConfig,
changedConfig,
changedSetting: "RequireMFA",
unrelatedBehaviors:
[
async config => await GetTokenBehaviorAsync(config),
async config => await GetSessionBehaviorAsync(config)
]);
// Assert
result.IsSuccess.Should().BeTrue(
because: "MFA should not affect token issuance or session management");
}
// Helper methods
private static Task<object> GetSessionBehaviorAsync(AuthorityTestConfig config)
{
return Task.FromResult<object>(new { MaxSessions = config.MaxConcurrentSessions });
}
private static Task<object> GetRefreshBehaviorAsync(AuthorityTestConfig config)
{
return Task.FromResult<object>(new { RefreshLifetime = config.RefreshTokenLifetimeHours });
}
private static Task<object> GetAuthenticationBehaviorAsync(AuthorityTestConfig config)
{
return Task.FromResult<object>(new { MfaRequired = config.RequireMFA });
}
private static Task<object> GetPasswordPolicyBehaviorAsync(AuthorityTestConfig config)
{
return Task.FromResult<object>(new { MinLength = config.MinPasswordLength });
}
private static Task<object> GetTokenBehaviorAsync(AuthorityTestConfig config)
{
return Task.FromResult<object>(new { Lifetime = config.AccessTokenLifetimeMinutes });
}
private static Task<BehaviorSnapshot> CaptureSessionBehaviorAsync(AuthorityTestConfig config)
{
var snapshot = new BehaviorSnapshot(
ConfigurationId: $"sessions-{config.MaxConcurrentSessions}",
Behaviors:
[
new CapturedBehavior("SessionLimit", config.MaxConcurrentSessions.ToString(), DateTimeOffset.UtcNow),
new CapturedBehavior("ConcurrencyPolicy",
config.MaxConcurrentSessions > 5 ? "permissive" : "restrictive", DateTimeOffset.UtcNow)
],
CapturedAt: DateTimeOffset.UtcNow);
return Task.FromResult(snapshot);
}
private static Task<BehaviorSnapshot> CapturePasswordPolicyBehaviorAsync(AuthorityTestConfig config)
{
var snapshot = new BehaviorSnapshot(
ConfigurationId: $"password-{config.MinPasswordLength}",
Behaviors:
[
new CapturedBehavior("PasswordComplexity",
config.MinPasswordLength >= 12 ? "enhanced" : "standard", DateTimeOffset.UtcNow),
new CapturedBehavior("ValidationRejectionRate",
config.MinPasswordLength >= 12 ? "increase" : "standard", DateTimeOffset.UtcNow)
],
CapturedAt: DateTimeOffset.UtcNow);
return Task.FromResult(snapshot);
}
}
/// <summary>
/// Test configuration for Authority module.
/// </summary>
public sealed record AuthorityTestConfig
{
public int AccessTokenLifetimeMinutes { get; init; } = 15;
public int RefreshTokenLifetimeHours { get; init; } = 24;
public int MaxConcurrentSessions { get; init; } = 5;
public bool EnableDPoP { get; init; } = false;
public int MinPasswordLength { get; init; } = 8;
public bool RequireMFA { get; init; } = false;
}

View File

@@ -0,0 +1,23 @@
<?xml version='1.0' encoding='utf-8'?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<LangVersion>preview</LangVersion>
<Description>Config-diff tests for Authority module</Description>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Options" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../__Libraries/StellaOps.Authority.Core/StellaOps.Authority.Core.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
<ProjectReference Include="../../../__Tests/__Libraries/StellaOps.Testing.ConfigDiff/StellaOps.Testing.ConfigDiff.csproj" />
</ItemGroup>
</Project>

View File

@@ -15,5 +15,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../__Libraries/StellaOps.Authority.Core/StellaOps.Authority.Core.csproj" />
<ProjectReference Include="../../../__Tests/__Libraries/StellaOps.Testing.Temporal/StellaOps.Testing.Temporal.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,296 @@
// <copyright file="TemporalVerdictTests.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
// </copyright>
// Sprint: SPRINT_20260105_002_001_TEST_time_skew_idempotency
// Task: TSKW-011
using FluentAssertions;
using StellaOps.Authority.Core.Verdicts;
using StellaOps.Testing.Temporal;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Authority.Core.Tests.Verdicts;
/// <summary>
/// Temporal testing for verdict manifests using the Testing.Temporal library.
/// Tests clock cutoff handling, timestamp consistency, and determinism under time skew.
/// </summary>
[Trait("Category", TestCategories.Unit)]
public sealed class TemporalVerdictTests
{
private static readonly DateTimeOffset BaseTime = new(2026, 1, 5, 12, 0, 0, TimeSpan.Zero);
[Fact]
public void VerdictManifest_ClockCutoff_BoundaryPrecision()
{
// Arrange
var ttlProvider = new TtlBoundaryTimeProvider(BaseTime);
var ttl = TimeSpan.FromHours(24); // Typical verdict validity window
var clockCutoff = BaseTime;
// Position at various boundaries
var testCases = TtlBoundaryTimeProvider.GenerateBoundaryTestCases(clockCutoff, ttl).ToList();
// Assert - verify all boundary cases are correctly handled
foreach (var testCase in testCases)
{
var isExpired = testCase.Time >= clockCutoff.Add(ttl);
isExpired.Should().Be(
testCase.ShouldBeExpired,
$"Verdict clock cutoff case '{testCase.Name}' should be expired={testCase.ShouldBeExpired}");
}
}
[Fact]
public void VerdictManifestBuilder_IsDeterministic_UnderTimeAdvancement()
{
// Arrange
var timeProvider = new SimulatedTimeProvider(BaseTime);
var results = new List<string>();
// Act - build multiple manifests while advancing time
for (int i = 0; i < 10; i++)
{
var manifest = BuildTestManifest(BaseTime); // Use fixed clock, not advancing
results.Add(manifest.ManifestDigest);
timeProvider.Advance(TimeSpan.FromMinutes(5)); // Advance between builds
}
// Assert - all manifests should have same digest (deterministic)
results.Distinct().Should().HaveCount(1, "manifests built with same inputs should be deterministic");
}
[Fact]
public void VerdictManifestBuilder_Build_IsIdempotent()
{
// Arrange
var stateSnapshotter = () => BuildTestManifest(BaseTime).ManifestDigest;
var verifier = new IdempotencyVerifier<string>(stateSnapshotter);
// Act - verify Build is idempotent
var result = verifier.Verify(() => { /* Build is called in snapshotter */ }, repetitions: 5);
// Assert
result.IsIdempotent.Should().BeTrue("VerdictManifestBuilder.Build should be idempotent");
result.AllSucceeded.Should().BeTrue();
}
[Fact]
public void VerdictManifest_TimestampOrdering_IsMonotonic()
{
// Arrange - simulate verdict timestamps
var timeProvider = new SimulatedTimeProvider(BaseTime);
var timestamps = new List<DateTimeOffset>();
// Simulate verdict lifecycle: created, processed, signed, stored
timestamps.Add(timeProvider.GetUtcNow()); // Created
timeProvider.Advance(TimeSpan.FromMilliseconds(50));
timestamps.Add(timeProvider.GetUtcNow()); // Processed
timeProvider.Advance(TimeSpan.FromMilliseconds(100));
timestamps.Add(timeProvider.GetUtcNow()); // Signed
timeProvider.Advance(TimeSpan.FromMilliseconds(20));
timestamps.Add(timeProvider.GetUtcNow()); // Stored
// Act & Assert - timestamps should be monotonically increasing
ClockSkewAssertions.AssertMonotonicTimestamps(timestamps);
}
[Fact]
public void VerdictManifest_HandlesClockSkewForward()
{
// Arrange
var timeProvider = new SimulatedTimeProvider(BaseTime);
var clockCutoff1 = timeProvider.GetUtcNow();
// Simulate clock jump forward (NTP correction)
timeProvider.JumpTo(BaseTime.AddHours(2));
var clockCutoff2 = timeProvider.GetUtcNow();
// Act - build manifests with different clock cutoffs
var manifest1 = BuildTestManifest(clockCutoff1);
var manifest2 = BuildTestManifest(clockCutoff2);
// Assert - different clock cutoffs should produce different digests
manifest1.ManifestDigest.Should().NotBe(manifest2.ManifestDigest,
"different clock cutoffs should produce different manifest digests");
// Clock cutoff difference should be within expected range
ClockSkewAssertions.AssertTimestampsWithinTolerance(
clockCutoff1,
clockCutoff2,
tolerance: TimeSpan.FromHours(3));
}
[Fact]
public void VerdictManifest_ClockDrift_DoesNotAffectDeterminism()
{
// Arrange
var timeProvider = new SimulatedTimeProvider(BaseTime);
timeProvider.SetDrift(TimeSpan.FromMilliseconds(10)); // 10ms/second drift
var results = new List<string>();
var fixedClock = BaseTime; // Use fixed clock for manifest
// Act - build manifests while time drifts
for (int i = 0; i < 10; i++)
{
var manifest = BuildTestManifest(fixedClock);
results.Add(manifest.ManifestDigest);
timeProvider.Advance(TimeSpan.FromSeconds(10)); // Time advances with drift
}
// Assert - all should be identical (fixed clock input)
results.Distinct().Should().HaveCount(1,
"manifests with fixed clock should be deterministic regardless of system drift");
}
[Fact]
public void VerdictManifest_ClockJumpBackward_IsDetected()
{
// Arrange
var timeProvider = new SimulatedTimeProvider(BaseTime);
var timestamps = new List<DateTimeOffset>();
// Record timestamps
timestamps.Add(timeProvider.GetUtcNow());
timeProvider.Advance(TimeSpan.FromMinutes(5));
timestamps.Add(timeProvider.GetUtcNow());
// Simulate clock jump backward
timeProvider.JumpBackward(TimeSpan.FromMinutes(3));
timestamps.Add(timeProvider.GetUtcNow());
// Assert - backward jump should be detected
timeProvider.HasJumpedBackward().Should().BeTrue();
// Non-monotonic timestamps should be detected
var act = () => ClockSkewAssertions.AssertMonotonicTimestamps(timestamps);
act.Should().Throw<ClockSkewAssertionException>();
}
[Theory]
[InlineData(0.9, VexStatus.NotAffected)]
[InlineData(0.7, VexStatus.Affected)]
[InlineData(0.5, VexStatus.UnderInvestigation)]
public void VerdictManifest_ConfidenceScores_AreIdempotent(double confidence, VexStatus status)
{
// Arrange
var stateSnapshotter = () =>
{
var manifest = BuildTestManifest(BaseTime, confidence, status);
return manifest.Result.Confidence;
};
var verifier = new IdempotencyVerifier<double>(stateSnapshotter);
// Act
var result = verifier.Verify(() => { }, repetitions: 3);
// Assert
result.IsIdempotent.Should().BeTrue();
result.States.Should().AllSatisfy(c => c.Should().Be(confidence));
}
[Fact]
public void VerdictManifest_ExpiryWindow_BoundaryTests()
{
// Arrange - simulate verdict expiry window (e.g., 7 days)
var expiryWindow = TimeSpan.FromDays(7);
var createdAt = BaseTime;
// Generate boundary test cases
var testCases = TtlBoundaryTimeProvider.GenerateBoundaryTestCases(createdAt, expiryWindow);
// Assert
foreach (var testCase in testCases)
{
var isExpired = testCase.Time >= createdAt.Add(expiryWindow);
isExpired.Should().Be(testCase.ShouldBeExpired, testCase.Name);
}
}
[Theory]
[MemberData(nameof(GetVerdictExpiryBoundaryData))]
public void VerdictManifest_TheoryBoundaryTests(
string name,
DateTimeOffset testTime,
bool shouldBeExpired)
{
// Arrange
var expiryWindow = TimeSpan.FromDays(7);
var expiry = BaseTime.Add(expiryWindow);
// Act
var isExpired = testTime >= expiry;
// Assert
isExpired.Should().Be(shouldBeExpired, $"Case '{name}' should be expired={shouldBeExpired}");
}
public static IEnumerable<object[]> GetVerdictExpiryBoundaryData()
{
var expiryWindow = TimeSpan.FromDays(7);
return TtlBoundaryTimeProvider.GenerateTheoryData(BaseTime, expiryWindow);
}
[Fact]
public void VerdictManifest_LeapSecondScenario_MaintainsDeterminism()
{
// Arrange
var leapDay = new DateOnly(2016, 12, 31);
var leapProvider = new LeapSecondTimeProvider(
new DateTimeOffset(2016, 12, 31, 23, 0, 0, TimeSpan.Zero),
leapDay);
var results = new List<string>();
var fixedClock = new DateTimeOffset(2016, 12, 31, 12, 0, 0, TimeSpan.Zero);
// Act - build manifests while advancing through leap second
foreach (var moment in leapProvider.AdvanceThroughLeapSecond(leapDay))
{
var manifest = BuildTestManifest(fixedClock);
results.Add(manifest.ManifestDigest);
}
// Assert - all manifests should be identical (fixed clock)
results.Distinct().Should().HaveCount(1,
"manifests should be deterministic even during leap second transition");
}
private static VerdictManifest BuildTestManifest(
DateTimeOffset clockCutoff,
double confidence = 0.85,
VexStatus status = VexStatus.NotAffected)
{
return new VerdictManifestBuilder(() => "test-manifest-id")
.WithTenant("tenant-1")
.WithAsset("sha256:abc123", "CVE-2024-1234")
.WithInputs(
sbomDigests: new[] { "sha256:sbom1" },
vulnFeedSnapshotIds: new[] { "feed-snapshot-1" },
vexDocumentDigests: new[] { "sha256:vex1" },
clockCutoff: clockCutoff)
.WithResult(
status: status,
confidence: confidence,
explanations: new[]
{
new VerdictExplanation
{
SourceId = "vendor-a",
Reason = "Test explanation",
ProvenanceScore = 0.9,
CoverageScore = 0.8,
ReplayabilityScore = 0.7,
StrengthMultiplier = 1.0,
FreshnessMultiplier = 0.95,
ClaimScore = confidence,
AssertedStatus = status,
Accepted = true,
},
})
.WithPolicy("sha256:policy123", "1.0.0")
.WithClock(clockCutoff)
.Build();
}
}