save progress

This commit is contained in:
StellaOps Bot
2026-01-02 21:06:27 +02:00
parent f46bde5575
commit 3f197814c5
441 changed files with 21545 additions and 4306 deletions

View File

@@ -0,0 +1,210 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Auth.ServerIntegration;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Auth.ServerIntegration.Tests;
public class StellaOpsAuthorityConfigurationManagerTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GetConfigurationAsync_UsesCacheUntilExpiry()
{
var now = DateTimeOffset.Parse("2025-01-01T00:00:00Z");
var timeProvider = new FakeTimeProvider(now);
var handler = new RecordingHandler();
handler.EnqueueMetadataResponse(CreateJsonResponse(DiscoveryDocument));
handler.EnqueueJwksResponse(CreateJsonResponse("{\"keys\":[]}"));
var options = CreateOptions("https://authority.test");
var optionsMonitor = new MutableOptionsMonitor<StellaOpsResourceServerOptions>(options);
var manager = new StellaOpsAuthorityConfigurationManager(
new TestHttpClientFactory(new HttpClient(handler)),
optionsMonitor,
timeProvider,
NullLogger<StellaOpsAuthorityConfigurationManager>.Instance);
var first = await manager.GetConfigurationAsync(CancellationToken.None);
var second = await manager.GetConfigurationAsync(CancellationToken.None);
Assert.Same(first, second);
Assert.Equal(1, handler.MetadataRequests);
Assert.Equal(1, handler.JwksRequests);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GetConfigurationAsync_UsesOfflineFallbackWhenRefreshFails()
{
var now = DateTimeOffset.Parse("2025-01-01T00:00:00Z");
var timeProvider = new FakeTimeProvider(now);
var handler = new RecordingHandler();
handler.EnqueueMetadataResponse(CreateJsonResponse(DiscoveryDocument));
handler.EnqueueJwksResponse(CreateJsonResponse("{\"keys\":[]}"));
handler.EnqueueMetadataResponse(_ => throw new HttpRequestException("offline"));
var options = CreateOptions("https://authority.test");
options.MetadataCacheLifetime = TimeSpan.FromMinutes(1);
options.OfflineCacheTolerance = TimeSpan.FromMinutes(5);
options.Validate();
var optionsMonitor = new MutableOptionsMonitor<StellaOpsResourceServerOptions>(options);
var manager = new StellaOpsAuthorityConfigurationManager(
new TestHttpClientFactory(new HttpClient(handler)),
optionsMonitor,
timeProvider,
NullLogger<StellaOpsAuthorityConfigurationManager>.Instance);
var first = await manager.GetConfigurationAsync(CancellationToken.None);
timeProvider.Advance(TimeSpan.FromMinutes(2));
var second = await manager.GetConfigurationAsync(CancellationToken.None);
Assert.Same(first, second);
Assert.Equal(2, handler.MetadataRequests);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GetConfigurationAsync_RefreshesWhenAuthorityChanges()
{
var now = DateTimeOffset.Parse("2025-01-01T00:00:00Z");
var timeProvider = new FakeTimeProvider(now);
var handler = new RecordingHandler();
handler.EnqueueMetadataResponse(CreateJsonResponse(DiscoveryDocument));
handler.EnqueueJwksResponse(CreateJsonResponse("{\"keys\":[]}"));
handler.EnqueueMetadataResponse(CreateJsonResponse(DiscoveryDocument));
handler.EnqueueJwksResponse(CreateJsonResponse("{\"keys\":[]}"));
var options = CreateOptions("https://authority.test");
var optionsMonitor = new MutableOptionsMonitor<StellaOpsResourceServerOptions>(options);
var manager = new StellaOpsAuthorityConfigurationManager(
new TestHttpClientFactory(new HttpClient(handler)),
optionsMonitor,
timeProvider,
NullLogger<StellaOpsAuthorityConfigurationManager>.Instance);
await manager.GetConfigurationAsync(CancellationToken.None);
var updated = CreateOptions("https://authority2.test");
optionsMonitor.Set(updated);
await manager.GetConfigurationAsync(CancellationToken.None);
Assert.Equal(2, handler.MetadataRequests);
}
private static StellaOpsResourceServerOptions CreateOptions(string authority)
{
var options = new StellaOpsResourceServerOptions
{
Authority = authority,
MetadataCacheLifetime = TimeSpan.FromMinutes(5),
OfflineCacheTolerance = TimeSpan.FromMinutes(10),
AllowOfflineCacheFallback = true
};
options.Validate();
return options;
}
private static HttpResponseMessage CreateJsonResponse(string json)
{
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(json)
{
Headers = { ContentType = new MediaTypeHeaderValue("application/json") }
}
};
}
private sealed class RecordingHandler : HttpMessageHandler
{
private readonly Queue<Func<HttpRequestMessage, HttpResponseMessage>> metadataResponses = new();
private readonly Queue<Func<HttpRequestMessage, HttpResponseMessage>> jwksResponses = new();
public int MetadataRequests { get; private set; }
public int JwksRequests { get; private set; }
public void EnqueueMetadataResponse(HttpResponseMessage response)
=> metadataResponses.Enqueue(_ => response);
public void EnqueueMetadataResponse(Func<HttpRequestMessage, HttpResponseMessage> factory)
=> metadataResponses.Enqueue(factory);
public void EnqueueJwksResponse(HttpResponseMessage response)
=> jwksResponses.Enqueue(_ => response);
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var uri = request.RequestUri?.AbsoluteUri ?? string.Empty;
if (uri.Contains("openid-configuration", StringComparison.OrdinalIgnoreCase))
{
MetadataRequests++;
return Task.FromResult(metadataResponses.Dequeue().Invoke(request));
}
if (uri.Contains("jwks", StringComparison.OrdinalIgnoreCase))
{
JwksRequests++;
return Task.FromResult(jwksResponses.Dequeue().Invoke(request));
}
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound));
}
}
private sealed class TestHttpClientFactory : IHttpClientFactory
{
private readonly HttpClient client;
public TestHttpClientFactory(HttpClient client)
{
this.client = client;
}
public HttpClient CreateClient(string name) => client;
}
private sealed class MutableOptionsMonitor<T> : IOptionsMonitor<T>
where T : class
{
private T value;
public MutableOptionsMonitor(T value)
{
this.value = value;
}
public T CurrentValue => value;
public T Get(string? name) => value;
public IDisposable OnChange(Action<T, string> listener) => NullDisposable.Instance;
public void Set(T newValue) => value = newValue;
private sealed class NullDisposable : IDisposable
{
public static NullDisposable Instance { get; } = new();
public void Dispose()
{
}
}
}
private const string DiscoveryDocument =
"{\"issuer\":\"https://authority.test\",\"authorization_endpoint\":\"https://authority.test/connect/authorize\",\"token_endpoint\":\"https://authority.test/connect/token\",\"jwks_uri\":\"https://authority.test/jwks\"}";
}

View File

@@ -0,0 +1,82 @@
using System.Collections.Generic;
using System.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Auth.ServerIntegration;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Auth.ServerIntegration.Tests;
public class StellaOpsBypassEvaluatorTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ShouldBypass_ReturnsFalse_WhenRemoteIpMissing()
{
var optionsMonitor = CreateOptionsMonitor(options =>
{
options.Authority = "https://authority.test";
options.BypassNetworks.Add("127.0.0.1/32");
options.Validate();
});
var evaluator = new StellaOpsBypassEvaluator(optionsMonitor, NullLogger<StellaOpsBypassEvaluator>.Instance);
var context = new DefaultHttpContext();
var result = evaluator.ShouldBypass(context, new List<string> { "scope" });
Assert.False(result);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ShouldBypass_ReturnsFalse_WhenAuthorizationHeaderPresent()
{
var optionsMonitor = CreateOptionsMonitor(options =>
{
options.Authority = "https://authority.test";
options.BypassNetworks.Add("127.0.0.1/32");
options.Validate();
});
var evaluator = new StellaOpsBypassEvaluator(optionsMonitor, NullLogger<StellaOpsBypassEvaluator>.Instance);
var context = new DefaultHttpContext();
context.Connection.RemoteIpAddress = IPAddress.Parse("127.0.0.1");
context.Request.Headers["Authorization"] = "Bearer token";
var result = evaluator.ShouldBypass(context, new List<string> { "scope" });
Assert.False(result);
}
private static IOptionsMonitor<StellaOpsResourceServerOptions> CreateOptionsMonitor(Action<StellaOpsResourceServerOptions> configure)
=> new TestOptionsMonitor<StellaOpsResourceServerOptions>(configure);
private sealed class TestOptionsMonitor<TOptions> : IOptionsMonitor<TOptions>
where TOptions : class, new()
{
private readonly TOptions value;
public TestOptionsMonitor(Action<TOptions> configure)
{
value = new TOptions();
configure(value);
}
public TOptions CurrentValue => value;
public TOptions Get(string? name) => value;
public IDisposable OnChange(Action<TOptions, string> listener) => NullDisposable.Instance;
private sealed class NullDisposable : IDisposable
{
public static NullDisposable Instance { get; } = new();
public void Dispose()
{
}
}
}
}

View File

@@ -55,4 +55,19 @@ public class StellaOpsResourceServerOptionsTests
Assert.Contains("Authority", exception.Message, StringComparison.OrdinalIgnoreCase);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_Throws_When_OfflineCacheToleranceInvalid()
{
var options = new StellaOpsResourceServerOptions
{
Authority = "https://authority.stella-ops.test",
OfflineCacheTolerance = TimeSpan.FromDays(2)
};
var exception = Assert.Throws<InvalidOperationException>(() => options.Validate());
Assert.Contains("offline cache tolerance", exception.Message, StringComparison.OrdinalIgnoreCase);
}
}

View File

@@ -54,6 +54,56 @@ public class StellaOpsScopeAuthorizationHandlerTests
Assert.False(string.IsNullOrWhiteSpace(record.CorrelationId));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task HandleRequirement_Succeeds_WhenScopeItemIsNormalized()
{
var optionsMonitor = CreateOptionsMonitor(options =>
{
options.Authority = "https://authority.example";
options.Validate();
});
var (handler, accessor, sink) = CreateHandler(optionsMonitor, remoteAddress: IPAddress.Parse("10.0.0.2"));
var requirement = new StellaOpsScopeRequirement(new[] { StellaOpsScopes.PolicyRun });
var principal = new StellaOpsPrincipalBuilder()
.WithSubject("user-2")
.AddClaim(StellaOpsClaimTypes.ScopeItem, " POLICY:RUN ")
.Build();
var context = new AuthorizationHandlerContext(new[] { requirement }, principal, accessor.HttpContext);
await handler.HandleAsync(context);
Assert.True(context.HasSucceeded);
Assert.Single(sink.Records);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task HandleRequirement_Succeeds_WhenScopeItemVulnReadMapsToVulnView()
{
var optionsMonitor = CreateOptionsMonitor(options =>
{
options.Authority = "https://authority.example";
options.Validate();
});
var (handler, accessor, sink) = CreateHandler(optionsMonitor, remoteAddress: IPAddress.Parse("10.0.0.3"));
var requirement = new StellaOpsScopeRequirement(new[] { StellaOpsScopes.VulnView });
var principal = new StellaOpsPrincipalBuilder()
.WithSubject("user-3")
.AddClaim(StellaOpsClaimTypes.ScopeItem, "VULN:READ")
.Build();
var context = new AuthorizationHandlerContext(new[] { requirement }, principal, accessor.HttpContext);
await handler.HandleAsync(context);
Assert.True(context.HasSucceeded);
Assert.Single(sink.Records);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task HandleRequirement_Fails_WhenTenantMismatch()