compose and authority fixes. finish sprints.

This commit is contained in:
master
2026-02-17 21:59:47 +02:00
parent fb46a927ad
commit 49cdebe2f1
187 changed files with 23189 additions and 1439 deletions

View File

@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -26,6 +27,7 @@ using StellaOps.Policy.Snapshots;
using StellaOps.Policy.ToolLattice;
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Http;
@@ -200,7 +202,29 @@ builder.Services.AddSingleton<IToolAccessEvaluator, ToolAccessEvaluator>();
builder.Services.AddStellaOpsResourceServerAuthentication(
builder.Configuration,
configurationSection: $"{PolicyGatewayOptions.SectionName}:ResourceServer");
configurationSection: $"{PolicyGatewayOptions.SectionName}:ResourceServer",
configure: resourceOptions =>
{
// IConfiguration binder does not always clear default list values.
// When local compose sets Audiences to an empty value, explicitly clear
// the audience list so no-aud local tokens can be validated.
var audiences = builder.Configuration
.GetSection($"{PolicyGatewayOptions.SectionName}:ResourceServer:Audiences")
.Get<string[]>();
if (audiences is null)
{
return;
}
resourceOptions.Audiences.Clear();
foreach (var audience in audiences)
{
if (!string.IsNullOrWhiteSpace(audience))
{
resourceOptions.Audiences.Add(audience.Trim());
}
}
});
// Accept self-signed certificates when HTTPS metadata validation is disabled (dev/Docker)
if (!bootstrap.Options.ResourceServer.RequireHttpsMetadata)
@@ -258,6 +282,11 @@ if (bootstrap.Options.PolicyEngine.ClientCredentials.Enabled)
.AddPolicyHandler(static (provider, _) => CreateAuthorityRetryPolicy(provider))
.AddHttpMessageHandler<PolicyGatewayDpopHandler>();
}
else
{
// Keep DI graph valid when client credentials are disabled.
builder.Services.AddSingleton<IStellaOpsTokenClient, DisabledStellaOpsTokenClient>();
}
builder.Services.AddHttpClient<IPolicyEngineClient, PolicyEngineClient>((serviceProvider, client) =>
{
@@ -295,6 +324,23 @@ app.MapGet("/readyz", () => Results.Ok(new { status = "ready" }))
app.MapGet("/", () => Results.Redirect("/healthz"));
app.MapGet("/api/policy/quota", ([FromServices] TimeProvider timeProvider) =>
{
var now = timeProvider.GetUtcNow();
var resetAt = now.Date.AddDays(1).ToString("O", CultureInfo.InvariantCulture);
return Results.Ok(new
{
simulationsPerDay = 1000,
simulationsUsed = 0,
evaluationsPerDay = 5000,
evaluationsUsed = 0,
resetAt
});
})
.WithTags("Policy Quota")
.WithName("PolicyQuota.Get")
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRead));
var policyPacks = app.MapGroup("/api/policy/packs")
.WithTags("Policy Packs");

View File

@@ -0,0 +1,39 @@
using Microsoft.IdentityModel.Tokens;
using StellaOps.Auth.Client;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Policy.Gateway.Services;
internal sealed class DisabledStellaOpsTokenClient : IStellaOpsTokenClient
{
private const string DisabledMessage = "Policy Engine client credentials are disabled.";
public Task<StellaOpsTokenResult> RequestPasswordTokenAsync(
string username,
string password,
string? scope = null,
IReadOnlyDictionary<string, string>? additionalParameters = null,
CancellationToken cancellationToken = default)
=> Task.FromException<StellaOpsTokenResult>(new InvalidOperationException(DisabledMessage));
public Task<StellaOpsTokenResult> RequestClientCredentialsTokenAsync(
string? scope = null,
IReadOnlyDictionary<string, string>? additionalParameters = null,
CancellationToken cancellationToken = default)
=> Task.FromException<StellaOpsTokenResult>(new InvalidOperationException(DisabledMessage));
public Task<JsonWebKeySet> GetJsonWebKeySetAsync(CancellationToken cancellationToken = default)
=> Task.FromException<JsonWebKeySet>(new InvalidOperationException(DisabledMessage));
public ValueTask<StellaOpsTokenCacheEntry?> GetCachedTokenAsync(string key, CancellationToken cancellationToken = default)
=> ValueTask.FromResult<StellaOpsTokenCacheEntry?>(null);
public ValueTask CacheTokenAsync(string key, StellaOpsTokenCacheEntry entry, CancellationToken cancellationToken = default)
=> ValueTask.CompletedTask;
public ValueTask ClearCachedTokenAsync(string key, CancellationToken cancellationToken = default)
=> ValueTask.CompletedTask;
}

View File

@@ -88,11 +88,22 @@ internal sealed class PolicyEngineTokenProvider
}
var scopeString = BuildScopeClaim(options);
var result = await tokenClient.RequestClientCredentialsTokenAsync(scopeString, null, cancellationToken).ConfigureAwait(false);
var expiresAt = result.ExpiresAtUtc;
cachedToken = new CachedToken(result.AccessToken, string.IsNullOrWhiteSpace(result.TokenType) ? "Bearer" : result.TokenType, expiresAt);
logger.LogInformation("Issued Policy Engine client credentials token; expires at {ExpiresAt:o}.", expiresAt);
return cachedToken;
try
{
var result = await tokenClient.RequestClientCredentialsTokenAsync(scopeString, null, cancellationToken).ConfigureAwait(false);
var expiresAt = result.ExpiresAtUtc;
cachedToken = new CachedToken(result.AccessToken, string.IsNullOrWhiteSpace(result.TokenType) ? "Bearer" : result.TokenType, expiresAt);
logger.LogInformation("Issued Policy Engine client credentials token; expires at {ExpiresAt:o}.", expiresAt);
return cachedToken;
}
catch (Exception ex)
{
logger.LogWarning(
ex,
"Unable to issue Policy Engine client credentials token for scopes '{Scopes}'.",
scopeString);
return null;
}
}
finally
{