Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
324 lines
10 KiB
C#
324 lines
10 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using Microsoft.Extensions.Logging;
|
|
using StellaOps.Auth.Abstractions;
|
|
|
|
namespace StellaOps.Policy.Gateway.Options;
|
|
|
|
/// <summary>
|
|
/// Root configuration for the Policy Gateway host.
|
|
/// </summary>
|
|
public sealed class PolicyGatewayOptions
|
|
{
|
|
public const string SectionName = "PolicyGateway";
|
|
|
|
public PolicyGatewayTelemetryOptions Telemetry { get; } = new();
|
|
|
|
public PolicyGatewayResourceServerOptions ResourceServer { get; } = new();
|
|
|
|
public PolicyGatewayPolicyEngineOptions PolicyEngine { get; } = new();
|
|
|
|
public void Validate()
|
|
{
|
|
Telemetry.Validate();
|
|
ResourceServer.Validate();
|
|
PolicyEngine.Validate();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Logging and telemetry configuration for the gateway.
|
|
/// </summary>
|
|
public sealed class PolicyGatewayTelemetryOptions
|
|
{
|
|
public LogLevel MinimumLogLevel { get; set; } = LogLevel.Information;
|
|
|
|
public void Validate()
|
|
{
|
|
if (!Enum.IsDefined(typeof(LogLevel), MinimumLogLevel))
|
|
{
|
|
throw new InvalidOperationException("Unsupported log level configured for Policy Gateway telemetry.");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// JWT resource server configuration for incoming requests handled by the gateway.
|
|
/// </summary>
|
|
public sealed class PolicyGatewayResourceServerOptions
|
|
{
|
|
public string Authority { get; set; } = "https://authority.stella-ops.local";
|
|
|
|
public string? MetadataAddress { get; set; }
|
|
= "https://authority.stella-ops.local/.well-known/openid-configuration";
|
|
|
|
public IList<string> Audiences { get; } = new List<string> { "api://policy-gateway" };
|
|
|
|
public IList<string> RequiredScopes { get; } = new List<string>
|
|
{
|
|
StellaOpsScopes.PolicyRead,
|
|
StellaOpsScopes.PolicyAuthor,
|
|
StellaOpsScopes.PolicyReview,
|
|
StellaOpsScopes.PolicyApprove,
|
|
StellaOpsScopes.PolicyOperate,
|
|
StellaOpsScopes.PolicySimulate,
|
|
StellaOpsScopes.PolicyRun,
|
|
StellaOpsScopes.PolicyActivate
|
|
};
|
|
|
|
public IList<string> RequiredTenants { get; } = new List<string>();
|
|
|
|
public IList<string> BypassNetworks { get; } = new List<string> { "127.0.0.1/32", "::1/128" };
|
|
|
|
public bool RequireHttpsMetadata { get; set; } = true;
|
|
|
|
public int BackchannelTimeoutSeconds { get; set; } = 30;
|
|
|
|
public int TokenClockSkewSeconds { get; set; } = 60;
|
|
|
|
public void Validate()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(Authority))
|
|
{
|
|
throw new InvalidOperationException("Policy Gateway resource server configuration requires an Authority URL.");
|
|
}
|
|
|
|
if (!Uri.TryCreate(Authority.Trim(), UriKind.Absolute, out var authorityUri))
|
|
{
|
|
throw new InvalidOperationException("Policy Gateway resource server Authority URL must be absolute.");
|
|
}
|
|
|
|
if (RequireHttpsMetadata &&
|
|
!authorityUri.IsLoopback &&
|
|
!string.Equals(authorityUri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
throw new InvalidOperationException("Policy Gateway resource server Authority URL must use HTTPS when metadata requires HTTPS.");
|
|
}
|
|
|
|
if (BackchannelTimeoutSeconds <= 0)
|
|
{
|
|
throw new InvalidOperationException("Policy Gateway resource server back-channel timeout must be greater than zero seconds.");
|
|
}
|
|
|
|
if (TokenClockSkewSeconds < 0 || TokenClockSkewSeconds > 300)
|
|
{
|
|
throw new InvalidOperationException("Policy Gateway resource server token clock skew must be between 0 and 300 seconds.");
|
|
}
|
|
|
|
NormalizeList(Audiences, toLower: false);
|
|
NormalizeList(RequiredScopes, toLower: true);
|
|
NormalizeList(RequiredTenants, toLower: true);
|
|
NormalizeList(BypassNetworks, toLower: false);
|
|
}
|
|
|
|
private static void NormalizeList(IList<string> values, bool toLower)
|
|
{
|
|
if (values.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var unique = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
for (var index = values.Count - 1; index >= 0; index--)
|
|
{
|
|
var value = values[index];
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
values.RemoveAt(index);
|
|
continue;
|
|
}
|
|
|
|
var normalized = value.Trim();
|
|
if (toLower)
|
|
{
|
|
normalized = normalized.ToLowerInvariant();
|
|
}
|
|
|
|
if (!unique.Add(normalized))
|
|
{
|
|
values.RemoveAt(index);
|
|
continue;
|
|
}
|
|
|
|
values[index] = normalized;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Outbound Policy Engine configuration used by the gateway to forward requests.
|
|
/// </summary>
|
|
public sealed class PolicyGatewayPolicyEngineOptions
|
|
{
|
|
public string BaseAddress { get; set; } = "https://policy-engine.stella-ops.local";
|
|
|
|
public string Audience { get; set; } = "api://policy-engine";
|
|
|
|
public PolicyGatewayClientCredentialsOptions ClientCredentials { get; } = new();
|
|
|
|
public PolicyGatewayDpopOptions Dpop { get; } = new();
|
|
|
|
public void Validate()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(BaseAddress))
|
|
{
|
|
throw new InvalidOperationException("Policy Gateway requires a Policy Engine base address.");
|
|
}
|
|
|
|
if (!Uri.TryCreate(BaseAddress.Trim(), UriKind.Absolute, out var baseUri))
|
|
{
|
|
throw new InvalidOperationException("Policy Gateway Policy Engine base address must be an absolute URI.");
|
|
}
|
|
|
|
if (!string.Equals(baseUri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) && !baseUri.IsLoopback)
|
|
{
|
|
throw new InvalidOperationException("Policy Gateway Policy Engine base address must use HTTPS unless targeting loopback.");
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(Audience))
|
|
{
|
|
throw new InvalidOperationException("Policy Gateway requires a Policy Engine audience value for client credential flows.");
|
|
}
|
|
|
|
ClientCredentials.Validate();
|
|
Dpop.Validate();
|
|
}
|
|
|
|
public Uri BaseUri => new(BaseAddress, UriKind.Absolute);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Client credential configuration for the gateway when calling the Policy Engine.
|
|
/// </summary>
|
|
public sealed class PolicyGatewayClientCredentialsOptions
|
|
{
|
|
public bool Enabled { get; set; } = true;
|
|
|
|
public string ClientId { get; set; } = "policy-gateway";
|
|
|
|
public string? ClientSecret { get; set; }
|
|
= "change-me";
|
|
|
|
public IList<string> Scopes { get; } = new List<string>
|
|
{
|
|
StellaOpsScopes.PolicyRead,
|
|
StellaOpsScopes.PolicyAuthor,
|
|
StellaOpsScopes.PolicyReview,
|
|
StellaOpsScopes.PolicyApprove,
|
|
StellaOpsScopes.PolicyOperate,
|
|
StellaOpsScopes.PolicySimulate,
|
|
StellaOpsScopes.PolicyRun,
|
|
StellaOpsScopes.PolicyActivate
|
|
};
|
|
|
|
public int BackchannelTimeoutSeconds { get; set; } = 30;
|
|
|
|
public void Validate()
|
|
{
|
|
if (!Enabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(ClientId))
|
|
{
|
|
throw new InvalidOperationException("Policy Gateway client credential configuration requires a client identifier when enabled.");
|
|
}
|
|
|
|
if (Scopes.Count == 0)
|
|
{
|
|
throw new InvalidOperationException("Policy Gateway client credential configuration requires at least one scope when enabled.");
|
|
}
|
|
|
|
var normalized = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
for (var index = Scopes.Count - 1; index >= 0; index--)
|
|
{
|
|
var scope = Scopes[index];
|
|
if (string.IsNullOrWhiteSpace(scope))
|
|
{
|
|
Scopes.RemoveAt(index);
|
|
continue;
|
|
}
|
|
|
|
var trimmed = scope.Trim().ToLowerInvariant();
|
|
if (!normalized.Add(trimmed))
|
|
{
|
|
Scopes.RemoveAt(index);
|
|
continue;
|
|
}
|
|
|
|
Scopes[index] = trimmed;
|
|
}
|
|
|
|
if (Scopes.Count == 0)
|
|
{
|
|
throw new InvalidOperationException("Policy Gateway client credential configuration requires at least one non-empty scope when enabled.");
|
|
}
|
|
|
|
if (BackchannelTimeoutSeconds <= 0)
|
|
{
|
|
throw new InvalidOperationException("Policy Gateway client credential back-channel timeout must be greater than zero seconds.");
|
|
}
|
|
}
|
|
|
|
public IReadOnlyList<string> NormalizedScopes => new ReadOnlyCollection<string>(Scopes);
|
|
|
|
public TimeSpan BackchannelTimeout => TimeSpan.FromSeconds(BackchannelTimeoutSeconds);
|
|
}
|
|
|
|
/// <summary>
|
|
/// DPoP sender-constrained credential configuration for outbound Policy Engine calls.
|
|
/// </summary>
|
|
public sealed class PolicyGatewayDpopOptions
|
|
{
|
|
public bool Enabled { get; set; } = false;
|
|
|
|
public string KeyPath { get; set; } = string.Empty;
|
|
|
|
public string? KeyPassphrase { get; set; }
|
|
= null;
|
|
|
|
public string Algorithm { get; set; } = "ES256";
|
|
|
|
public TimeSpan ProofLifetime { get; set; } = TimeSpan.FromMinutes(2);
|
|
|
|
public TimeSpan ClockSkew { get; set; } = TimeSpan.FromSeconds(30);
|
|
|
|
public void Validate()
|
|
{
|
|
if (!Enabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(KeyPath))
|
|
{
|
|
throw new InvalidOperationException("Policy Gateway DPoP configuration requires a key path when enabled.");
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(Algorithm))
|
|
{
|
|
throw new InvalidOperationException("Policy Gateway DPoP configuration requires an algorithm when enabled.");
|
|
}
|
|
|
|
var normalizedAlgorithm = Algorithm.Trim().ToUpperInvariant();
|
|
if (normalizedAlgorithm is not ("ES256" or "ES384"))
|
|
{
|
|
throw new InvalidOperationException("Policy Gateway DPoP configuration supports only ES256 or ES384 algorithms.");
|
|
}
|
|
|
|
if (ProofLifetime <= TimeSpan.Zero)
|
|
{
|
|
throw new InvalidOperationException("Policy Gateway DPoP proof lifetime must be greater than zero.");
|
|
}
|
|
|
|
if (ClockSkew < TimeSpan.Zero || ClockSkew > TimeSpan.FromMinutes(5))
|
|
{
|
|
throw new InvalidOperationException("Policy Gateway DPoP clock skew must be between 0 seconds and 5 minutes.");
|
|
}
|
|
|
|
Algorithm = normalizedAlgorithm;
|
|
}
|
|
}
|