Restructure solution layout by module
This commit is contained in:
@@ -0,0 +1,323 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user