tests fixes and some product advisories tunes ups

This commit is contained in:
master
2026-01-30 07:57:43 +02:00
parent 644887997c
commit 55744f6a39
345 changed files with 26290 additions and 2267 deletions

View File

@@ -4,6 +4,7 @@ using System.Collections.Immutable;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
@@ -63,6 +64,7 @@ using System.Diagnostics.Metrics;
using StellaOps.Concelier.Models.Observations;
using StellaOps.Aoc.AspNetCore.Results;
using HttpResults = Microsoft.AspNetCore.Http.Results;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.Advisories;
using StellaOps.Concelier.Storage.Aliases;
using StellaOps.Provenance;
@@ -141,10 +143,16 @@ if (builder.Environment.IsEnvironment("Testing"))
},
Telemetry = new ConcelierOptions.TelemetryOptions
{
Enabled = false
Enabled = false,
EnableLogging = false // Disable Serilog so test's LoggerProvider is used
}
};
// Ensure Serilog is disabled in Testing so test's LoggerProvider captures logs
concelierOptions.Telemetry ??= new ConcelierOptions.TelemetryOptions();
concelierOptions.Telemetry.Enabled = false;
concelierOptions.Telemetry.EnableLogging = false;
concelierOptions.PostgresStorage ??= new ConcelierOptions.PostgresStorageOptions
{
Enabled = true,
@@ -158,6 +166,231 @@ if (builder.Environment.IsEnvironment("Testing"))
concelierOptions.PostgresStorage.ConnectionString = Environment.GetEnvironmentVariable("CONCELIER_TEST_STORAGE_DSN") ?? string.Empty;
}
// Read Evidence.Root from env var if provided (used by test fixture for attestation tests)
var evidenceRootEnv = Environment.GetEnvironmentVariable("CONCELIER_EVIDENCE__ROOT");
if (!string.IsNullOrWhiteSpace(evidenceRootEnv))
{
concelierOptions.Evidence ??= new ConcelierOptions.EvidenceBundleOptions();
concelierOptions.Evidence.Root = evidenceRootEnv;
}
// Read Features settings from env vars (used by tests for feature flag testing)
concelierOptions.Features ??= new ConcelierOptions.FeaturesOptions();
var noMergeEnabledEnv = Environment.GetEnvironmentVariable("CONCELIER_FEATURES__NOMERGEENABLED");
if (!string.IsNullOrWhiteSpace(noMergeEnabledEnv))
{
concelierOptions.Features.NoMergeEnabled = string.Equals(noMergeEnabledEnv, "true", StringComparison.OrdinalIgnoreCase);
}
// Read MergeJobAllowlist from env vars (array format: CONCELIER_FEATURES__MERGEJOBALLOWLIST__0, __1, etc.)
for (int i = 0; i < 10; i++)
{
var allowlistItem = Environment.GetEnvironmentVariable($"CONCELIER_FEATURES__MERGEJOBALLOWLIST__{i}");
if (string.IsNullOrWhiteSpace(allowlistItem))
break;
concelierOptions.Features.MergeJobAllowlist.Add(allowlistItem);
}
// Read Mirror settings from env vars (used by mirror endpoint tests)
var mirrorEnabledEnv = Environment.GetEnvironmentVariable("CONCELIER_MIRROR__ENABLED");
if (!string.IsNullOrWhiteSpace(mirrorEnabledEnv))
{
concelierOptions.Mirror ??= new ConcelierOptions.MirrorOptions();
concelierOptions.Mirror.Enabled = string.Equals(mirrorEnabledEnv, "true", StringComparison.OrdinalIgnoreCase);
}
var mirrorExportRootEnv = Environment.GetEnvironmentVariable("CONCELIER_MIRROR__EXPORTROOT");
if (!string.IsNullOrWhiteSpace(mirrorExportRootEnv))
{
concelierOptions.Mirror ??= new ConcelierOptions.MirrorOptions();
concelierOptions.Mirror.ExportRoot = mirrorExportRootEnv;
}
var mirrorActiveExportIdEnv = Environment.GetEnvironmentVariable("CONCELIER_MIRROR__ACTIVEEXPORTID");
if (!string.IsNullOrWhiteSpace(mirrorActiveExportIdEnv))
{
concelierOptions.Mirror ??= new ConcelierOptions.MirrorOptions();
concelierOptions.Mirror.ActiveExportId = mirrorActiveExportIdEnv;
}
var mirrorMaxIndexEnv = Environment.GetEnvironmentVariable("CONCELIER_MIRROR__MAXINDEXREQUESTSPERHOUR");
if (!string.IsNullOrWhiteSpace(mirrorMaxIndexEnv) && int.TryParse(mirrorMaxIndexEnv, out var maxIndexReqs))
{
concelierOptions.Mirror ??= new ConcelierOptions.MirrorOptions();
concelierOptions.Mirror.MaxIndexRequestsPerHour = maxIndexReqs;
}
// Read Mirror Domains array from env vars
for (int i = 0; i < 10; i++)
{
var domainId = Environment.GetEnvironmentVariable($"CONCELIER_MIRROR__DOMAINS__{i}__ID");
if (string.IsNullOrWhiteSpace(domainId))
break;
concelierOptions.Mirror ??= new ConcelierOptions.MirrorOptions();
var domain = new ConcelierOptions.MirrorDomainOptions { Id = domainId };
var domainRequireAuth = Environment.GetEnvironmentVariable($"CONCELIER_MIRROR__DOMAINS__{i}__REQUIREAUTHENTICATION");
if (!string.IsNullOrWhiteSpace(domainRequireAuth))
{
domain.RequireAuthentication = string.Equals(domainRequireAuth, "true", StringComparison.OrdinalIgnoreCase);
}
var domainMaxDownload = Environment.GetEnvironmentVariable($"CONCELIER_MIRROR__DOMAINS__{i}__MAXDOWNLOADREQUESTSPERHOUR");
if (!string.IsNullOrWhiteSpace(domainMaxDownload) && int.TryParse(domainMaxDownload, out var maxDownloadReqs))
{
domain.MaxDownloadRequestsPerHour = maxDownloadReqs;
}
concelierOptions.Mirror.Domains.Add(domain);
}
// Read Authority settings from env vars (used by auth tests)
var authorityEnabledEnv = Environment.GetEnvironmentVariable("CONCELIER_AUTHORITY__ENABLED");
if (!string.IsNullOrWhiteSpace(authorityEnabledEnv))
{
concelierOptions.Authority ??= new ConcelierOptions.AuthorityOptions();
concelierOptions.Authority.Enabled = string.Equals(authorityEnabledEnv, "true", StringComparison.OrdinalIgnoreCase);
}
var authorityIssuerEnv = Environment.GetEnvironmentVariable("CONCELIER_AUTHORITY__ISSUER");
if (!string.IsNullOrWhiteSpace(authorityIssuerEnv))
{
concelierOptions.Authority ??= new ConcelierOptions.AuthorityOptions();
concelierOptions.Authority.Issuer = authorityIssuerEnv;
}
var authorityAllowAnonEnv = Environment.GetEnvironmentVariable("CONCELIER_AUTHORITY__ALLOWANONYMOUSFALLBACK");
if (!string.IsNullOrWhiteSpace(authorityAllowAnonEnv))
{
concelierOptions.Authority ??= new ConcelierOptions.AuthorityOptions();
concelierOptions.Authority.AllowAnonymousFallback = string.Equals(authorityAllowAnonEnv, "true", StringComparison.OrdinalIgnoreCase);
}
var authorityRequireHttpsEnv = Environment.GetEnvironmentVariable("CONCELIER_AUTHORITY__REQUIREHTTPSMETADATA");
if (!string.IsNullOrWhiteSpace(authorityRequireHttpsEnv))
{
concelierOptions.Authority ??= new ConcelierOptions.AuthorityOptions();
concelierOptions.Authority.RequireHttpsMetadata = string.Equals(authorityRequireHttpsEnv, "true", StringComparison.OrdinalIgnoreCase);
}
var authorityClientIdEnv = Environment.GetEnvironmentVariable("CONCELIER_AUTHORITY__CLIENTID");
if (!string.IsNullOrWhiteSpace(authorityClientIdEnv))
{
concelierOptions.Authority ??= new ConcelierOptions.AuthorityOptions();
concelierOptions.Authority.ClientId = authorityClientIdEnv;
}
var authorityClientSecretEnv = Environment.GetEnvironmentVariable("CONCELIER_AUTHORITY__CLIENTSECRET");
if (!string.IsNullOrWhiteSpace(authorityClientSecretEnv))
{
concelierOptions.Authority ??= new ConcelierOptions.AuthorityOptions();
concelierOptions.Authority.ClientSecret = authorityClientSecretEnv;
}
// Read Authority Audiences array from env vars
for (int i = 0; i < 10; i++)
{
var audience = Environment.GetEnvironmentVariable($"CONCELIER_AUTHORITY__AUDIENCES__{i}");
if (string.IsNullOrWhiteSpace(audience))
break;
concelierOptions.Authority ??= new ConcelierOptions.AuthorityOptions();
concelierOptions.Authority.Audiences ??= new List<string>();
concelierOptions.Authority.Audiences.Add(audience);
}
// Read Authority RequiredScopes array from env vars
for (int i = 0; i < 10; i++)
{
var scope = Environment.GetEnvironmentVariable($"CONCELIER_AUTHORITY__REQUIREDSCOPES__{i}");
if (string.IsNullOrWhiteSpace(scope))
break;
concelierOptions.Authority ??= new ConcelierOptions.AuthorityOptions();
concelierOptions.Authority.RequiredScopes ??= new List<string>();
if (!concelierOptions.Authority.RequiredScopes.Contains(scope))
{
concelierOptions.Authority.RequiredScopes.Add(scope);
}
}
// Read Authority ClientScopes array from env vars
for (int i = 0; i < 10; i++)
{
var scope = Environment.GetEnvironmentVariable($"CONCELIER_AUTHORITY__CLIENTSCOPES__{i}");
if (string.IsNullOrWhiteSpace(scope))
break;
concelierOptions.Authority ??= new ConcelierOptions.AuthorityOptions();
concelierOptions.Authority.ClientScopes ??= new List<string>();
if (!concelierOptions.Authority.ClientScopes.Contains(scope))
{
concelierOptions.Authority.ClientScopes.Add(scope);
}
}
// Read Authority RequiredTenants array from env vars
for (int i = 0; i < 10; i++)
{
var tenant = Environment.GetEnvironmentVariable($"CONCELIER_AUTHORITY__REQUIREDTENANTS__{i}");
if (string.IsNullOrWhiteSpace(tenant))
break;
concelierOptions.Authority ??= new ConcelierOptions.AuthorityOptions();
concelierOptions.Authority.RequiredTenants ??= new List<string>();
if (!concelierOptions.Authority.RequiredTenants.Contains(tenant, StringComparer.OrdinalIgnoreCase))
{
concelierOptions.Authority.RequiredTenants.Add(tenant);
}
}
// Read Authority BypassNetworks array from env vars (used for IP-based auth bypass)
for (int i = 0; i < 10; i++)
{
var network = Environment.GetEnvironmentVariable($"CONCELIER_AUTHORITY__BYPASSNETWORKS__{i}");
if (string.IsNullOrWhiteSpace(network))
break;
concelierOptions.Authority ??= new ConcelierOptions.AuthorityOptions();
concelierOptions.Authority.BypassNetworks ??= new List<string>();
if (!concelierOptions.Authority.BypassNetworks.Contains(network, StringComparer.OrdinalIgnoreCase))
{
concelierOptions.Authority.BypassNetworks.Add(network);
}
}
// Read Authority TestSigningSecret from env var
var authorityTestSigningSecretEnv = Environment.GetEnvironmentVariable("CONCELIER_AUTHORITY__TESTSIGNINGSECRET");
if (!string.IsNullOrWhiteSpace(authorityTestSigningSecretEnv))
{
concelierOptions.Authority ??= new ConcelierOptions.AuthorityOptions();
concelierOptions.Authority.TestSigningSecret = authorityTestSigningSecretEnv;
}
// Read Authority BackchannelTimeoutSeconds from env var
var authorityBackchannelTimeoutEnv = Environment.GetEnvironmentVariable("CONCELIER_AUTHORITY__BACKCHANNELTIMEOUTSECONDS");
if (!string.IsNullOrWhiteSpace(authorityBackchannelTimeoutEnv) && int.TryParse(authorityBackchannelTimeoutEnv, out var backchannelTimeout))
{
concelierOptions.Authority ??= new ConcelierOptions.AuthorityOptions();
concelierOptions.Authority.BackchannelTimeoutSeconds = backchannelTimeout;
}
// Read Authority Resilience options from env vars
var resilienceEnableRetriesEnv = Environment.GetEnvironmentVariable("CONCELIER_AUTHORITY__RESILIENCE__ENABLERETRIES");
if (!string.IsNullOrWhiteSpace(resilienceEnableRetriesEnv))
{
concelierOptions.Authority ??= new ConcelierOptions.AuthorityOptions();
concelierOptions.Authority.Resilience ??= new ConcelierOptions.AuthorityOptions.ResilienceOptions();
concelierOptions.Authority.Resilience.EnableRetries = string.Equals(resilienceEnableRetriesEnv, "true", StringComparison.OrdinalIgnoreCase);
}
// Read Resilience RetryDelays array from env vars
for (int i = 0; i < 10; i++)
{
var delayStr = Environment.GetEnvironmentVariable($"CONCELIER_AUTHORITY__RESILIENCE__RETRYDELAYS__{i}");
if (string.IsNullOrWhiteSpace(delayStr))
break;
if (TimeSpan.TryParse(delayStr, out var delay))
{
concelierOptions.Authority ??= new ConcelierOptions.AuthorityOptions();
concelierOptions.Authority.Resilience ??= new ConcelierOptions.AuthorityOptions.ResilienceOptions();
concelierOptions.Authority.Resilience.RetryDelays ??= new List<TimeSpan>();
concelierOptions.Authority.Resilience.RetryDelays.Add(delay);
}
}
var resilienceAllowOfflineCacheEnv = Environment.GetEnvironmentVariable("CONCELIER_AUTHORITY__RESILIENCE__ALLOWOFFLINECACHEFALLBACK");
if (!string.IsNullOrWhiteSpace(resilienceAllowOfflineCacheEnv))
{
concelierOptions.Authority ??= new ConcelierOptions.AuthorityOptions();
concelierOptions.Authority.Resilience ??= new ConcelierOptions.AuthorityOptions.ResilienceOptions();
concelierOptions.Authority.Resilience.AllowOfflineCacheFallback = string.Equals(resilienceAllowOfflineCacheEnv, "true", StringComparison.OrdinalIgnoreCase);
}
var resilienceOfflineCacheToleranceEnv = Environment.GetEnvironmentVariable("CONCELIER_AUTHORITY__RESILIENCE__OFFLINECACHETOLERANCE");
if (!string.IsNullOrWhiteSpace(resilienceOfflineCacheToleranceEnv) && TimeSpan.TryParse(resilienceOfflineCacheToleranceEnv, out var offlineTolerance))
{
concelierOptions.Authority ??= new ConcelierOptions.AuthorityOptions();
concelierOptions.Authority.Resilience ??= new ConcelierOptions.AuthorityOptions.ResilienceOptions();
concelierOptions.Authority.Resilience.OfflineCacheTolerance = offlineTolerance;
}
ConcelierOptionsPostConfigure.Apply(concelierOptions, contentRootPath);
concelierOptions.Authority ??= new ConcelierOptions.AuthorityOptions();
concelierOptions.Authority.RequiredScopes ??= new List<string>();
@@ -179,6 +412,10 @@ if (builder.Environment.IsEnvironment("Testing"))
{
concelierOptions.Authority.ClientScopes.Add(StellaOpsScopes.ConcelierJobsTrigger);
}
// Register in-memory storage stubs for Testing to satisfy merge module dependencies
builder.Services.AddInMemoryStorage();
// Skip validation in Testing to allow factory-provided wiring.
}
else
@@ -214,6 +451,7 @@ else
// Register the chosen options instance so downstream services/tests share it.
builder.Services.AddSingleton(concelierOptions);
builder.Services.AddSingleton<IOptions<ConcelierOptions>>(_ => Microsoft.Extensions.Options.Options.Create(concelierOptions));
builder.Services.AddSingleton<IOptionsMonitor<ConcelierOptions>>(_ => new StaticOptionsMonitor<ConcelierOptions>(concelierOptions));
builder.Services.AddStellaOpsCrypto(concelierOptions.Crypto);
@@ -381,8 +619,11 @@ if (authorityConfigured)
}
});
Console.WriteLine($"[DEBUG] Authority.TestSigningSecret is empty: {string.IsNullOrWhiteSpace(concelierOptions.Authority.TestSigningSecret)}");
Console.WriteLine($"[DEBUG] Authority.TestSigningSecret length: {concelierOptions.Authority.TestSigningSecret?.Length ?? 0}");
if (string.IsNullOrWhiteSpace(concelierOptions.Authority.TestSigningSecret))
{
Console.WriteLine("[DEBUG] Taking OIDC discovery branch");
builder.Services.AddStellaOpsResourceServerAuthentication(
builder.Configuration,
configurationSection: null,
@@ -416,21 +657,27 @@ if (authorityConfigured)
}
else
{
// TestSigningSecret branch: used for integration tests with symmetric key signing.
// Validation is relaxed since this is only used in controlled test environments.
Console.WriteLine("[DEBUG] Taking TestSigningSecret branch (symmetric key)");
Console.WriteLine($"[DEBUG] TestSigningSecret value: {concelierOptions.Authority.TestSigningSecret}");
builder.Services
.AddAuthentication(StellaOpsAuthenticationDefaults.AuthenticationScheme)
.AddJwtBearer(StellaOpsAuthenticationDefaults.AuthenticationScheme, options =>
{
options.RequireHttpsMetadata = concelierOptions.Authority.RequireHttpsMetadata;
options.RequireHttpsMetadata = false;
options.MapInboundClaims = false;
#pragma warning disable CS0618 // Type or member is obsolete - UseSecurityTokenValidators is needed for compatibility with test tokens created using JwtSecurityTokenHandler
options.UseSecurityTokenValidators = true;
#pragma warning restore CS0618
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(concelierOptions.Authority.TestSigningSecret!)),
ValidateIssuer = true,
ValidIssuer = concelierOptions.Authority.Issuer,
ValidateAudience = concelierOptions.Authority.Audiences.Count > 0,
ValidAudiences = concelierOptions.Authority.Audiences,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromSeconds(concelierOptions.Authority.TokenClockSkewSeconds),
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = false,
ClockSkew = TimeSpan.FromMinutes(5),
NameClaimType = StellaOpsClaimTypes.Subject,
RoleClaimType = ClaimTypes.Role
};
@@ -474,11 +721,74 @@ if (authorityConfigured)
}
context.Token = token;
logger.LogInformation("JWT token received for {Path}, length={Length}", context.HttpContext.Request.Path, token.Length);
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<Program>>();
logger.LogError(context.Exception, "JWT authentication failed for {Path}: {Error}", context.HttpContext.Request.Path, context.Exception?.Message);
return Task.CompletedTask;
},
OnTokenValidated = context =>
{
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<Program>>();
logger.LogInformation("JWT token validated for {Path}, principal={Principal}", context.HttpContext.Request.Path, context.Principal?.Identity?.Name ?? "anonymous");
return Task.CompletedTask;
}
};
});
// Register authorization handler and bypass evaluator (same as AddStellaOpsResourceServerAuthentication)
builder.Services.AddHttpContextAccessor();
builder.Services.AddStellaOpsScopeHandler();
builder.Services.TryAddSingleton<StellaOpsBypassEvaluator>();
builder.Services.TryAddSingleton<TimeProvider>(_ => TimeProvider.System);
builder.Services.AddOptions<StellaOpsResourceServerOptions>()
.PostConfigure<IOptions<ConcelierOptions>>((resourceOptions, concelierOptionsSnapshot) =>
{
var authority = concelierOptionsSnapshot.Value.Authority ?? new ConcelierOptions.AuthorityOptions();
resourceOptions.Authority = authority.Issuer;
resourceOptions.RequireHttpsMetadata = authority.RequireHttpsMetadata;
resourceOptions.BackchannelTimeout = TimeSpan.FromSeconds(authority.BackchannelTimeoutSeconds);
resourceOptions.TokenClockSkew = TimeSpan.FromSeconds(authority.TokenClockSkewSeconds);
foreach (var audience in authority.Audiences)
{
if (!resourceOptions.Audiences.Contains(audience))
{
resourceOptions.Audiences.Add(audience);
}
}
foreach (var scope in authority.RequiredScopes)
{
if (!resourceOptions.RequiredScopes.Contains(scope))
{
resourceOptions.RequiredScopes.Add(scope);
}
}
foreach (var network in authority.BypassNetworks)
{
if (!resourceOptions.BypassNetworks.Contains(network))
{
resourceOptions.BypassNetworks.Add(network);
}
}
foreach (var tenant in authority.RequiredTenants)
{
if (!resourceOptions.RequiredTenants.Contains(tenant))
{
resourceOptions.RequiredTenants.Add(tenant);
}
}
// Validate to populate BypassMatcher and normalized collections
resourceOptions.Validate();
});
}
}
@@ -511,6 +821,8 @@ var resolvedConcelierOptions = app.Services.GetRequiredService<IOptions<Concelie
var resolvedAuthority = resolvedConcelierOptions.Authority ?? new ConcelierOptions.AuthorityOptions();
authorityConfigured = resolvedAuthority.Enabled;
var enforceAuthority = resolvedAuthority.Enabled && !resolvedAuthority.AllowAnonymousFallback;
var bypassMatcher = new NetworkMaskMatcher(resolvedAuthority.BypassNetworks ?? Array.Empty<string>());
var authorizationAuditLogger = app.Services.GetRequiredService<ILoggerFactory>().CreateLogger("Concelier.Authorization.Audit");
var requiredTenants = (resolvedAuthority.RequiredTenants ?? Array.Empty<string>())
.Select(static tenant => tenant?.Trim().ToLowerInvariant())
.Where(static tenant => !string.IsNullOrWhiteSpace(tenant))
@@ -527,6 +839,34 @@ if (resolvedAuthority.Enabled && resolvedAuthority.AllowAnonymousFallback)
if (authorityConfigured)
{
app.UseAuthentication();
// Middleware to log authorization denied results (BEFORE UseAuthorization so it wraps around it)
app.Use(async (context, next) =>
{
var auditLogger = context.RequestServices
.GetRequiredService<ILoggerFactory>()
.CreateLogger("Concelier.Authorization.Audit");
await next();
if (context.Response.StatusCode == (int)HttpStatusCode.Unauthorized)
{
var remoteAddress = context.Connection.RemoteIpAddress;
var bypassNetworks = resolvedAuthority.BypassNetworks ?? Array.Empty<string>();
var matcher = new NetworkMaskMatcher(bypassNetworks);
var bypassAllowed = matcher.IsAllowed(remoteAddress);
var isAuthenticated = context.User?.Identity?.IsAuthenticated ?? false;
auditLogger.LogWarning(
"Concelier authorization denied route={Route} status={StatusCode} bypassAllowed={BypassAllowed} hasPrincipal={HasPrincipal} remote={RemoteAddress}",
context.Request.Path.Value ?? string.Empty,
context.Response.StatusCode,
bypassAllowed,
isAuthenticated,
remoteAddress?.ToString() ?? "unknown");
}
});
app.UseAuthorization();
}
@@ -915,7 +1255,7 @@ app.MapGet("/v1/lnm/linksets", async (
foreach (var linkset in result.Items)
{
var summary = await BuildObservationSummaryAsync(observationQueryService, tenant!, linkset, cancellationToken).ConfigureAwait(false);
items.Add(ToLnmResponse(linkset, includeConflicts.GetValueOrDefault(true), includeTimeline: false, includeObservations: false, summary));
items.Add(ToLnmResponse(linkset, includeConflicts.GetValueOrDefault(true), includeTimeline: true, includeObservations: false, summary));
}
return HttpResults.Ok(new LnmLinksetPage(items, resolvedPage, resolvedPageSize, result.Total));
@@ -2953,15 +3293,39 @@ IResult? EnsureTenantAuthorized(HttpContext context, string tenant)
}
var principal = context.User;
var isAuthenticated = principal?.Identity?.IsAuthenticated == true;
var remoteAddress = context.Connection.RemoteIpAddress;
if (enforceAuthority && (principal?.Identity?.IsAuthenticated != true))
// Get bypass networks from request-scoped options to ensure PostConfigure has applied
var requestOptions = context.RequestServices.GetRequiredService<IOptions<ConcelierOptions>>().Value;
var requestBypassNetworks = requestOptions.Authority?.BypassNetworks ?? Array.Empty<string>();
var requestMatcher = new NetworkMaskMatcher(requestBypassNetworks);
var bypassAllowed = !isAuthenticated && requestMatcher.IsAllowed(remoteAddress);
if (enforceAuthority && !isAuthenticated && !bypassAllowed)
{
authorizationAuditLogger.LogWarning(
"Concelier authorization denied route={Route} remote={RemoteAddress} bypassAllowed={BypassAllowed} hasPrincipal={HasPrincipal}",
context.Request.Path.Value ?? string.Empty,
remoteAddress?.ToString() ?? "unknown",
bypassAllowed,
isAuthenticated);
return HttpResults.Unauthorized();
}
if (principal?.Identity?.IsAuthenticated == true)
if (bypassAllowed)
{
var tenantClaim = principal.FindFirstValue(StellaOpsClaimTypes.Tenant);
authorizationAuditLogger.LogInformation(
"Concelier authorization bypass granted route={Route} status={StatusCode} bypass={Bypass} remote={RemoteAddress}",
context.Request.Path.Value ?? string.Empty,
(int)HttpStatusCode.OK,
true,
remoteAddress?.ToString() ?? "unknown");
}
if (isAuthenticated)
{
var tenantClaim = principal!.FindFirstValue(StellaOpsClaimTypes.Tenant);
if (string.IsNullOrWhiteSpace(tenantClaim))
{
return HttpResults.Forbid();
@@ -4223,3 +4587,31 @@ static async Task<(bool Ready, TimeSpan Latency, string? Error)> CheckPostgresAs
}
}
/// <summary>
/// Static options monitor implementation for test scenarios where options are pre-configured.
/// </summary>
internal sealed class StaticOptionsMonitor<TOptions> : IOptionsMonitor<TOptions>
where TOptions : class
{
private readonly TOptions _value;
public StaticOptionsMonitor(TOptions value)
{
_value = value ?? throw new ArgumentNullException(nameof(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 readonly NullDisposable Instance = new();
public void Dispose()
{
}
}
}