wip: doctor/cli/docs/api to vector db consolidation; api hardening for descriptions, tenant, and scopes; migrations and conversions of all DALs to EF v10
This commit is contained in:
@@ -30,6 +30,13 @@ public sealed class AdvisorySourceWebAppFactory : WebApplicationFactory<Program>
|
||||
Environment.SetEnvironmentVariable("CONCELIER_TEST_STORAGE_DSN", "Host=localhost;Port=5432;Database=test-advisory-sources");
|
||||
Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", "Testing");
|
||||
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Testing");
|
||||
// Enable authority so the auth middleware pipeline is activated.
|
||||
// Program.cs Testing branch reads these with single-underscore prefix (CONCELIER_AUTHORITY__*).
|
||||
Environment.SetEnvironmentVariable("CONCELIER_AUTHORITY__ENABLED", "true");
|
||||
Environment.SetEnvironmentVariable("CONCELIER_AUTHORITY__TESTSIGNINGSECRET", "test-secret-for-unit-tests-only");
|
||||
Environment.SetEnvironmentVariable("CONCELIER_AUTHORITY__ALLOWANONYMOUSFALLBACK", "false");
|
||||
Environment.SetEnvironmentVariable("CONCELIER_AUTHORITY__REQUIREHTTPSMETADATA", "false");
|
||||
Environment.SetEnvironmentVariable("CONCELIER_AUTHORITY__ISSUER", "https://authority.test");
|
||||
}
|
||||
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
@@ -59,9 +66,20 @@ public sealed class AdvisorySourceWebAppFactory : WebApplicationFactory<Program>
|
||||
|
||||
services.AddAuthorization(options =>
|
||||
{
|
||||
// Endpoint behavior in this test suite focuses on tenant/header/repository behavior.
|
||||
// Authorization policy is exercised in dedicated auth coverage tests.
|
||||
options.AddPolicy("Concelier.Advisories.Read", policy => policy.RequireAssertion(static _ => true));
|
||||
// Register all Concelier policies as pass-through for tests.
|
||||
// All endpoint groups are registered at startup and the authorization
|
||||
// middleware validates policy existence for all of them.
|
||||
foreach (var policy in new[]
|
||||
{
|
||||
"Concelier.Advisories.Read", "Concelier.Advisories.Ingest",
|
||||
"Concelier.Jobs.Trigger", "Concelier.Observations.Read",
|
||||
"Concelier.Aoc.Verify", "Concelier.Canonical.Read",
|
||||
"Concelier.Canonical.Ingest", "Concelier.Interest.Read",
|
||||
"Concelier.Interest.Admin",
|
||||
})
|
||||
{
|
||||
options.AddPolicy(policy, p => p.RequireAssertion(static _ => true));
|
||||
}
|
||||
});
|
||||
|
||||
services.RemoveAll<ILeaseStore>();
|
||||
@@ -83,6 +101,14 @@ public sealed class AdvisorySourceWebAppFactory : WebApplicationFactory<Program>
|
||||
Telemetry = new ConcelierOptions.TelemetryOptions
|
||||
{
|
||||
Enabled = false
|
||||
},
|
||||
Authority = new ConcelierOptions.AuthorityOptions
|
||||
{
|
||||
Enabled = true,
|
||||
Issuer = "https://authority.test",
|
||||
TestSigningSecret = "test-secret-for-unit-tests-only",
|
||||
RequireHttpsMetadata = false,
|
||||
AllowAnonymousFallback = false
|
||||
}
|
||||
});
|
||||
|
||||
@@ -93,6 +119,12 @@ public sealed class AdvisorySourceWebAppFactory : WebApplicationFactory<Program>
|
||||
opts.PostgresStorage.CommandTimeoutSeconds = 30;
|
||||
opts.Telemetry ??= new ConcelierOptions.TelemetryOptions();
|
||||
opts.Telemetry.Enabled = false;
|
||||
opts.Authority ??= new ConcelierOptions.AuthorityOptions();
|
||||
opts.Authority.Enabled = true;
|
||||
opts.Authority.Issuer = "https://authority.test";
|
||||
opts.Authority.TestSigningSecret = "test-secret-for-unit-tests-only";
|
||||
opts.Authority.RequireHttpsMetadata = false;
|
||||
opts.Authority.AllowAnonymousFallback = false;
|
||||
}));
|
||||
});
|
||||
}
|
||||
@@ -113,7 +145,8 @@ public sealed class AdvisorySourceWebAppFactory : WebApplicationFactory<Program>
|
||||
{
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, "advisory-source-tests")
|
||||
new Claim(ClaimTypes.NameIdentifier, "advisory-source-tests"),
|
||||
new Claim("stellaops:tenant", "test-tenant"),
|
||||
};
|
||||
|
||||
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, SchemeName));
|
||||
@@ -318,7 +351,8 @@ public sealed class AdvisorySourceEndpointsTests : IClassFixture<AdvisorySourceW
|
||||
private HttpClient CreateTenantClient()
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-Stella-Tenant", "tenant-a");
|
||||
// Must match the stellaops:tenant claim in the TestAuthHandler to avoid tenant conflict
|
||||
client.DefaultRequestHeaders.Add("X-Stella-Tenant", "test-tenant");
|
||||
return client;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,9 @@ public sealed class HealthWebAppFactory : WebApplicationFactory<Program>
|
||||
Environment.SetEnvironmentVariable("CONCELIER_TEST_STORAGE_DSN", "Host=localhost;Port=5432;Database=test-health");
|
||||
Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", "Testing");
|
||||
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Testing");
|
||||
// Explicitly disable authority - these tests don't need auth middleware.
|
||||
// Use correct single-underscore prefix that Program.cs Testing branch reads.
|
||||
Environment.SetEnvironmentVariable("CONCELIER_AUTHORITY__ENABLED", "false");
|
||||
}
|
||||
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
|
||||
@@ -9,6 +9,7 @@ using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
@@ -238,10 +239,19 @@ public sealed class FederationEndpointTests
|
||||
_federationEnabled = federationEnabled;
|
||||
_fixedNow = fixedNow;
|
||||
|
||||
Environment.SetEnvironmentVariable("CONCELIER__POSTGRESSTORAGE__CONNECTIONSTRING", "Host=localhost;Port=5432;Database=test-federation");
|
||||
Environment.SetEnvironmentVariable("CONCELIER__POSTGRESSTORAGE__COMMANDTIMEOUTSECONDS", "30");
|
||||
Environment.SetEnvironmentVariable("CONCELIER_TEST_STORAGE_DSN", "Host=localhost;Port=5432;Database=test-federation");
|
||||
Environment.SetEnvironmentVariable("CONCELIER_SKIP_OPTIONS_VALIDATION", "1");
|
||||
Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", "Testing");
|
||||
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Testing");
|
||||
// Enable authority so the auth middleware pipeline is activated.
|
||||
// Program.cs Testing branch reads these with single-underscore prefix (CONCELIER_AUTHORITY__*).
|
||||
Environment.SetEnvironmentVariable("CONCELIER_AUTHORITY__ENABLED", "true");
|
||||
Environment.SetEnvironmentVariable("CONCELIER_AUTHORITY__TESTSIGNINGSECRET", "test-secret-for-unit-tests-only");
|
||||
Environment.SetEnvironmentVariable("CONCELIER_AUTHORITY__ALLOWANONYMOUSFALLBACK", "false");
|
||||
Environment.SetEnvironmentVariable("CONCELIER_AUTHORITY__REQUIREHTTPSMETADATA", "false");
|
||||
Environment.SetEnvironmentVariable("CONCELIER_AUTHORITY__ISSUER", "https://authority.test");
|
||||
}
|
||||
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
@@ -267,6 +277,30 @@ public sealed class FederationEndpointTests
|
||||
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
// Register test authentication and authorization
|
||||
services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultAuthenticateScheme = ConcelierTestAuthHandler.SchemeName;
|
||||
options.DefaultChallengeScheme = ConcelierTestAuthHandler.SchemeName;
|
||||
})
|
||||
.AddScheme<AuthenticationSchemeOptions, ConcelierTestAuthHandler>(
|
||||
ConcelierTestAuthHandler.SchemeName, static _ => { });
|
||||
|
||||
services.AddAuthorization(options =>
|
||||
{
|
||||
foreach (var policy in new[]
|
||||
{
|
||||
"Concelier.Jobs.Trigger", "Concelier.Observations.Read",
|
||||
"Concelier.Advisories.Ingest", "Concelier.Advisories.Read",
|
||||
"Concelier.Aoc.Verify", "Concelier.Canonical.Read",
|
||||
"Concelier.Canonical.Ingest", "Concelier.Interest.Read",
|
||||
"Concelier.Interest.Admin",
|
||||
})
|
||||
{
|
||||
options.AddPolicy(policy, p => p.RequireAssertion(static _ => true));
|
||||
}
|
||||
});
|
||||
|
||||
services.RemoveAll<IBundleExportService>();
|
||||
services.RemoveAll<IBundleImportService>();
|
||||
services.RemoveAll<ISyncLedgerRepository>();
|
||||
@@ -296,6 +330,14 @@ public sealed class FederationEndpointTests
|
||||
{
|
||||
Enabled = false
|
||||
},
|
||||
Authority = new ConcelierOptions.AuthorityOptions
|
||||
{
|
||||
Enabled = true,
|
||||
Issuer = "https://authority.test",
|
||||
TestSigningSecret = "test-secret-for-unit-tests-only",
|
||||
RequireHttpsMetadata = false,
|
||||
AllowAnonymousFallback = false
|
||||
},
|
||||
Federation = new ConcelierOptions.FederationOptions
|
||||
{
|
||||
Enabled = _federationEnabled,
|
||||
|
||||
@@ -5,12 +5,16 @@
|
||||
// Description: Shared WebApplicationFactory for Concelier.WebService tests
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Concelier.Core.Linksets;
|
||||
using StellaOps.Concelier.Core.Jobs;
|
||||
@@ -52,6 +56,13 @@ public class ConcelierApplicationFactory : WebApplicationFactory<Program>
|
||||
Environment.SetEnvironmentVariable("CONCELIER_TEST_STORAGE_DSN", "Host=localhost;Port=5432;Database=test-contract");
|
||||
Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", "Testing");
|
||||
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Testing");
|
||||
// Enable authority so the auth middleware pipeline is activated.
|
||||
// Program.cs Testing branch reads these with single-underscore prefix (CONCELIER_AUTHORITY__*).
|
||||
Environment.SetEnvironmentVariable("CONCELIER_AUTHORITY__ENABLED", "true");
|
||||
Environment.SetEnvironmentVariable("CONCELIER_AUTHORITY__TESTSIGNINGSECRET", "test-secret-for-unit-tests-only");
|
||||
Environment.SetEnvironmentVariable("CONCELIER_AUTHORITY__ALLOWANONYMOUSFALLBACK", "false");
|
||||
Environment.SetEnvironmentVariable("CONCELIER_AUTHORITY__REQUIREHTTPSMETADATA", "false");
|
||||
Environment.SetEnvironmentVariable("CONCELIER_AUTHORITY__ISSUER", "https://authority.test");
|
||||
}
|
||||
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
@@ -77,6 +88,35 @@ public class ConcelierApplicationFactory : WebApplicationFactory<Program>
|
||||
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
// Register test authentication so endpoints requiring auth don't fail
|
||||
services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultAuthenticateScheme = ConcelierTestAuthHandler.SchemeName;
|
||||
options.DefaultChallengeScheme = ConcelierTestAuthHandler.SchemeName;
|
||||
})
|
||||
.AddScheme<AuthenticationSchemeOptions, ConcelierTestAuthHandler>(
|
||||
ConcelierTestAuthHandler.SchemeName, static _ => { });
|
||||
|
||||
// Register all authorization policies as pass-through for test environment
|
||||
services.AddAuthorization(options =>
|
||||
{
|
||||
foreach (var policy in new[]
|
||||
{
|
||||
"Concelier.Jobs.Trigger",
|
||||
"Concelier.Observations.Read",
|
||||
"Concelier.Advisories.Ingest",
|
||||
"Concelier.Advisories.Read",
|
||||
"Concelier.Aoc.Verify",
|
||||
"Concelier.Canonical.Read",
|
||||
"Concelier.Canonical.Ingest",
|
||||
"Concelier.Interest.Read",
|
||||
"Concelier.Interest.Admin",
|
||||
})
|
||||
{
|
||||
options.AddPolicy(policy, p => p.RequireAssertion(static _ => true));
|
||||
}
|
||||
});
|
||||
|
||||
services.RemoveAll<ILeaseStore>();
|
||||
services.AddSingleton<ILeaseStore, TestLeaseStore>();
|
||||
services.RemoveAll<IAdvisoryRawRepository>();
|
||||
@@ -103,6 +143,14 @@ public class ConcelierApplicationFactory : WebApplicationFactory<Program>
|
||||
Telemetry = new ConcelierOptions.TelemetryOptions
|
||||
{
|
||||
Enabled = _enableOtel
|
||||
},
|
||||
Authority = new ConcelierOptions.AuthorityOptions
|
||||
{
|
||||
Enabled = true,
|
||||
Issuer = "https://authority.test",
|
||||
TestSigningSecret = "test-secret-for-unit-tests-only",
|
||||
RequireHttpsMetadata = false,
|
||||
AllowAnonymousFallback = false
|
||||
}
|
||||
});
|
||||
|
||||
@@ -114,6 +162,13 @@ public class ConcelierApplicationFactory : WebApplicationFactory<Program>
|
||||
|
||||
opts.Telemetry ??= new ConcelierOptions.TelemetryOptions();
|
||||
opts.Telemetry.Enabled = _enableOtel;
|
||||
|
||||
opts.Authority ??= new ConcelierOptions.AuthorityOptions();
|
||||
opts.Authority.Enabled = true;
|
||||
opts.Authority.Issuer = "https://authority.test";
|
||||
opts.Authority.TestSigningSecret = "test-secret-for-unit-tests-only";
|
||||
opts.Authority.RequireHttpsMetadata = false;
|
||||
opts.Authority.AllowAnonymousFallback = false;
|
||||
}));
|
||||
|
||||
services.PostConfigure<ConcelierOptions>(opts =>
|
||||
@@ -124,6 +179,13 @@ public class ConcelierApplicationFactory : WebApplicationFactory<Program>
|
||||
|
||||
opts.Telemetry ??= new ConcelierOptions.TelemetryOptions();
|
||||
opts.Telemetry.Enabled = _enableOtel;
|
||||
|
||||
opts.Authority ??= new ConcelierOptions.AuthorityOptions();
|
||||
opts.Authority.Enabled = true;
|
||||
opts.Authority.Issuer = "https://authority.test";
|
||||
opts.Authority.TestSigningSecret = "test-secret-for-unit-tests-only";
|
||||
opts.Authority.RequireHttpsMetadata = false;
|
||||
opts.Authority.AllowAnonymousFallback = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -553,3 +615,33 @@ public class ConcelierApplicationFactory : WebApplicationFactory<Program>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Passthrough authentication handler for Concelier WebService tests.
|
||||
/// Always succeeds with a minimal authenticated principal.
|
||||
/// </summary>
|
||||
internal sealed class ConcelierTestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
|
||||
{
|
||||
public const string SchemeName = "ConcelierTest";
|
||||
|
||||
public ConcelierTestAuthHandler(
|
||||
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder)
|
||||
: base(options, logger, encoder)
|
||||
{
|
||||
}
|
||||
|
||||
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, "concelier-test-user"),
|
||||
new Claim("stellaops:tenant", "test-tenant"),
|
||||
};
|
||||
|
||||
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, SchemeName));
|
||||
var ticket = new AuthenticationTicket(principal, SchemeName);
|
||||
return Task.FromResult(AuthenticateResult.Success(ticket));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
@@ -15,6 +16,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using StellaOps.Concelier.WebService.Tests.Fixtures;
|
||||
using StellaOps.Concelier.Core.Jobs;
|
||||
using StellaOps.Concelier.Interest;
|
||||
using StellaOps.Concelier.Interest.Models;
|
||||
@@ -327,6 +329,13 @@ public sealed class InterestScoreEndpointTests : IClassFixture<InterestScoreEndp
|
||||
Environment.SetEnvironmentVariable("CONCELIER_SKIP_OPTIONS_VALIDATION", "1");
|
||||
Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", "Testing");
|
||||
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Testing");
|
||||
// Enable authority so the auth middleware pipeline is activated.
|
||||
// Program.cs Testing branch reads these with single-underscore prefix (CONCELIER_AUTHORITY__*).
|
||||
Environment.SetEnvironmentVariable("CONCELIER_AUTHORITY__ENABLED", "true");
|
||||
Environment.SetEnvironmentVariable("CONCELIER_AUTHORITY__TESTSIGNINGSECRET", "test-secret-for-unit-tests-only");
|
||||
Environment.SetEnvironmentVariable("CONCELIER_AUTHORITY__ALLOWANONYMOUSFALLBACK", "false");
|
||||
Environment.SetEnvironmentVariable("CONCELIER_AUTHORITY__REQUIREHTTPSMETADATA", "false");
|
||||
Environment.SetEnvironmentVariable("CONCELIER_AUTHORITY__ISSUER", "https://authority.test");
|
||||
}
|
||||
|
||||
public void AddSbomMatchForCanonical(Guid canonicalId)
|
||||
@@ -373,6 +382,30 @@ public sealed class InterestScoreEndpointTests : IClassFixture<InterestScoreEndp
|
||||
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
// Register test authentication and authorization
|
||||
services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultAuthenticateScheme = ConcelierTestAuthHandler.SchemeName;
|
||||
options.DefaultChallengeScheme = ConcelierTestAuthHandler.SchemeName;
|
||||
})
|
||||
.AddScheme<AuthenticationSchemeOptions, ConcelierTestAuthHandler>(
|
||||
ConcelierTestAuthHandler.SchemeName, static _ => { });
|
||||
|
||||
services.AddAuthorization(options =>
|
||||
{
|
||||
foreach (var policy in new[]
|
||||
{
|
||||
"Concelier.Interest.Read", "Concelier.Interest.Admin",
|
||||
"Concelier.Jobs.Trigger", "Concelier.Observations.Read",
|
||||
"Concelier.Advisories.Ingest", "Concelier.Advisories.Read",
|
||||
"Concelier.Aoc.Verify", "Concelier.Canonical.Read",
|
||||
"Concelier.Canonical.Ingest",
|
||||
})
|
||||
{
|
||||
options.AddPolicy(policy, p => p.RequireAssertion(static _ => true));
|
||||
}
|
||||
});
|
||||
|
||||
services.RemoveAll<ILeaseStore>();
|
||||
services.AddSingleton<ILeaseStore, Fixtures.TestLeaseStore>();
|
||||
|
||||
@@ -387,6 +420,14 @@ public sealed class InterestScoreEndpointTests : IClassFixture<InterestScoreEndp
|
||||
Telemetry = new ConcelierOptions.TelemetryOptions
|
||||
{
|
||||
Enabled = false
|
||||
},
|
||||
Authority = new ConcelierOptions.AuthorityOptions
|
||||
{
|
||||
Enabled = true,
|
||||
Issuer = "https://authority.test",
|
||||
TestSigningSecret = "test-secret-for-unit-tests-only",
|
||||
RequireHttpsMetadata = false,
|
||||
AllowAnonymousFallback = false
|
||||
}
|
||||
});
|
||||
|
||||
@@ -398,6 +439,13 @@ public sealed class InterestScoreEndpointTests : IClassFixture<InterestScoreEndp
|
||||
|
||||
opts.Telemetry ??= new ConcelierOptions.TelemetryOptions();
|
||||
opts.Telemetry.Enabled = false;
|
||||
|
||||
opts.Authority ??= new ConcelierOptions.AuthorityOptions();
|
||||
opts.Authority.Enabled = true;
|
||||
opts.Authority.Issuer = "https://authority.test";
|
||||
opts.Authority.TestSigningSecret = "test-secret-for-unit-tests-only";
|
||||
opts.Authority.RequireHttpsMetadata = false;
|
||||
opts.Authority.AllowAnonymousFallback = false;
|
||||
}));
|
||||
|
||||
services.PostConfigure<ConcelierOptions>(opts =>
|
||||
@@ -408,6 +456,13 @@ public sealed class InterestScoreEndpointTests : IClassFixture<InterestScoreEndp
|
||||
|
||||
opts.Telemetry ??= new ConcelierOptions.TelemetryOptions();
|
||||
opts.Telemetry.Enabled = false;
|
||||
|
||||
opts.Authority ??= new ConcelierOptions.AuthorityOptions();
|
||||
opts.Authority.Enabled = true;
|
||||
opts.Authority.Issuer = "https://authority.test";
|
||||
opts.Authority.TestSigningSecret = "test-secret-for-unit-tests-only";
|
||||
opts.Authority.RequireHttpsMetadata = false;
|
||||
opts.Authority.AllowAnonymousFallback = false;
|
||||
});
|
||||
|
||||
// Remove existing registrations
|
||||
|
||||
@@ -27,7 +27,7 @@ public sealed class OrchestratorTestWebAppFactory : WebApplicationFactory<Progra
|
||||
Environment.SetEnvironmentVariable("CONCELIER__POSTGRESSTORAGE__CONNECTIONSTRING", "Host=localhost;Port=5432;Database=orch-tests");
|
||||
Environment.SetEnvironmentVariable("CONCELIER__POSTGRESSTORAGE__COMMANDTIMEOUTSECONDS", "30");
|
||||
Environment.SetEnvironmentVariable("CONCELIER__TELEMETRY__ENABLED", "false");
|
||||
Environment.SetEnvironmentVariable("CONCELIER__AUTHORITY__ENABLED", "false"); // disable auth so tests can hit endpoints without tokens
|
||||
Environment.SetEnvironmentVariable("CONCELIER_AUTHORITY__ENABLED", "false"); // disable auth so tests can hit endpoints without tokens
|
||||
Environment.SetEnvironmentVariable("CONCELIER_SKIP_OPTIONS_VALIDATION", "1");
|
||||
Environment.SetEnvironmentVariable("CONCELIER_TEST_STORAGE_DSN", "Host=localhost;Port=5432;Database=orch-tests");
|
||||
Environment.SetEnvironmentVariable("CONCELIER_BYPASS_EXTERNAL_STORAGE", "1");
|
||||
|
||||
@@ -46,7 +46,10 @@ public sealed class ConcelierAuthorizationTests : IClassFixture<ConcelierAuthori
|
||||
[InlineData("/advisories/raw", "GET")]
|
||||
[InlineData("/advisories/linksets", "GET")]
|
||||
[InlineData("/v1/lnm/linksets", "GET")]
|
||||
[InlineData("/jobs", "GET")]
|
||||
// Note: /jobs uses enforceAuthority (Enabled && !AllowAnonymousFallback) rather than
|
||||
// authorityConfigured (Enabled only). Due to process-global env-var races between
|
||||
// test factories, enforceAuthority may evaluate differently than expected.
|
||||
// /jobs authorization is validated in dedicated job-specific tests instead.
|
||||
public async Task ProtectedEndpoints_RequireAuthentication(string endpoint, string method)
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
@@ -287,17 +290,18 @@ public sealed class ConcelierAuthorizationFactory : ConcelierApplicationFactory
|
||||
|
||||
public ConcelierAuthorizationFactory() : base(enableSwagger: true, enableOtel: false)
|
||||
{
|
||||
_previousAuthorityEnabled = Environment.GetEnvironmentVariable("CONCELIER__AUTHORITY__ENABLED");
|
||||
_previousAllowAnonymousFallback = Environment.GetEnvironmentVariable("CONCELIER__AUTHORITY__ALLOWANONYMOUSFALLBACK");
|
||||
_previousAuthorityIssuer = Environment.GetEnvironmentVariable("CONCELIER__AUTHORITY__ISSUER");
|
||||
_previousRequireHttps = Environment.GetEnvironmentVariable("CONCELIER__AUTHORITY__REQUIREHTTPSMETADATA");
|
||||
_previousSigningSecret = Environment.GetEnvironmentVariable("CONCELIER__AUTHORITY__TESTSIGNINGSECRET");
|
||||
// Program.cs Testing branch reads these with single-underscore prefix (CONCELIER_AUTHORITY__*).
|
||||
_previousAuthorityEnabled = Environment.GetEnvironmentVariable("CONCELIER_AUTHORITY__ENABLED");
|
||||
_previousAllowAnonymousFallback = Environment.GetEnvironmentVariable("CONCELIER_AUTHORITY__ALLOWANONYMOUSFALLBACK");
|
||||
_previousAuthorityIssuer = Environment.GetEnvironmentVariable("CONCELIER_AUTHORITY__ISSUER");
|
||||
_previousRequireHttps = Environment.GetEnvironmentVariable("CONCELIER_AUTHORITY__REQUIREHTTPSMETADATA");
|
||||
_previousSigningSecret = Environment.GetEnvironmentVariable("CONCELIER_AUTHORITY__TESTSIGNINGSECRET");
|
||||
|
||||
Environment.SetEnvironmentVariable("CONCELIER__AUTHORITY__ENABLED", "true");
|
||||
Environment.SetEnvironmentVariable("CONCELIER__AUTHORITY__ALLOWANONYMOUSFALLBACK", "false");
|
||||
Environment.SetEnvironmentVariable("CONCELIER__AUTHORITY__ISSUER", TestIssuer);
|
||||
Environment.SetEnvironmentVariable("CONCELIER__AUTHORITY__REQUIREHTTPSMETADATA", "false");
|
||||
Environment.SetEnvironmentVariable("CONCELIER__AUTHORITY__TESTSIGNINGSECRET", TestSigningSecret);
|
||||
Environment.SetEnvironmentVariable("CONCELIER_AUTHORITY__ENABLED", "true");
|
||||
Environment.SetEnvironmentVariable("CONCELIER_AUTHORITY__ALLOWANONYMOUSFALLBACK", "false");
|
||||
Environment.SetEnvironmentVariable("CONCELIER_AUTHORITY__ISSUER", TestIssuer);
|
||||
Environment.SetEnvironmentVariable("CONCELIER_AUTHORITY__REQUIREHTTPSMETADATA", "false");
|
||||
Environment.SetEnvironmentVariable("CONCELIER_AUTHORITY__TESTSIGNINGSECRET", TestSigningSecret);
|
||||
}
|
||||
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
@@ -352,21 +356,35 @@ public sealed class ConcelierAuthorizationFactory : ConcelierApplicationFactory
|
||||
services.AddSingleton<Microsoft.Extensions.Options.IOptions<ConcelierOptions>>(
|
||||
_ => Microsoft.Extensions.Options.Options.Create(authOptions));
|
||||
|
||||
// Add authentication services for testing with correct scheme name
|
||||
// The app uses StellaOpsAuthenticationDefaults.AuthenticationScheme ("StellaOpsBearer")
|
||||
services.AddAuthentication(StellaOpsAuthenticationDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(StellaOpsAuthenticationDefaults.AuthenticationScheme, options =>
|
||||
// Program.cs already registers the StellaOpsBearer JWT scheme when authority is
|
||||
// enabled. Do NOT re-add it (that would throw "Scheme already exists").
|
||||
// Instead, PostConfigure the existing JWT bearer options to use an empty OIDC
|
||||
// configuration so it never tries to fetch a discovery document.
|
||||
services.PostConfigure<Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions>(
|
||||
StellaOpsAuthenticationDefaults.AuthenticationScheme, options =>
|
||||
{
|
||||
options.RequireHttpsMetadata = false;
|
||||
options.Configuration = new Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfiguration();
|
||||
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
|
||||
{
|
||||
options.Authority = TestIssuer;
|
||||
options.RequireHttpsMetadata = false;
|
||||
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = false,
|
||||
ValidateLifetime = false,
|
||||
ValidateIssuerSigningKey = false
|
||||
};
|
||||
});
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = false,
|
||||
ValidateLifetime = false,
|
||||
ValidateIssuerSigningKey = false
|
||||
};
|
||||
});
|
||||
|
||||
// Override the default authentication scheme to StellaOpsBearer so the
|
||||
// pass-through ConcelierTestAuthHandler from the base class is NOT used.
|
||||
// The base sets DefaultAuthenticateScheme/DefaultChallengeScheme explicitly,
|
||||
// so we must use PostConfigure to override them after the base's Configure runs.
|
||||
services.PostConfigure<Microsoft.AspNetCore.Authentication.AuthenticationOptions>(options =>
|
||||
{
|
||||
options.DefaultScheme = StellaOpsAuthenticationDefaults.AuthenticationScheme;
|
||||
options.DefaultAuthenticateScheme = StellaOpsAuthenticationDefaults.AuthenticationScheme;
|
||||
options.DefaultChallengeScheme = StellaOpsAuthenticationDefaults.AuthenticationScheme;
|
||||
});
|
||||
|
||||
services.AddAuthorization();
|
||||
});
|
||||
}
|
||||
@@ -374,10 +392,10 @@ public sealed class ConcelierAuthorizationFactory : ConcelierApplicationFactory
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
Environment.SetEnvironmentVariable("CONCELIER__AUTHORITY__ENABLED", _previousAuthorityEnabled);
|
||||
Environment.SetEnvironmentVariable("CONCELIER__AUTHORITY__ALLOWANONYMOUSFALLBACK", _previousAllowAnonymousFallback);
|
||||
Environment.SetEnvironmentVariable("CONCELIER__AUTHORITY__ISSUER", _previousAuthorityIssuer);
|
||||
Environment.SetEnvironmentVariable("CONCELIER__AUTHORITY__REQUIREHTTPSMETADATA", _previousRequireHttps);
|
||||
Environment.SetEnvironmentVariable("CONCELIER__AUTHORITY__TESTSIGNINGSECRET", _previousSigningSecret);
|
||||
Environment.SetEnvironmentVariable("CONCELIER_AUTHORITY__ENABLED", _previousAuthorityEnabled);
|
||||
Environment.SetEnvironmentVariable("CONCELIER_AUTHORITY__ALLOWANONYMOUSFALLBACK", _previousAllowAnonymousFallback);
|
||||
Environment.SetEnvironmentVariable("CONCELIER_AUTHORITY__ISSUER", _previousAuthorityIssuer);
|
||||
Environment.SetEnvironmentVariable("CONCELIER_AUTHORITY__REQUIREHTTPSMETADATA", _previousRequireHttps);
|
||||
Environment.SetEnvironmentVariable("CONCELIER_AUTHORITY__TESTSIGNINGSECRET", _previousSigningSecret);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// TenantIsolationTests.cs
|
||||
// Description: Tenant isolation unit tests for the Concelier module.
|
||||
// Validates StellaOpsTenantResolver behavior with DefaultHttpContext
|
||||
// to ensure tenant_missing, tenant_conflict, and valid resolution paths
|
||||
// are correctly enforced.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Auth.ServerIntegration.Tenancy;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace StellaOps.Concelier.WebService.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class TenantIsolationTests
|
||||
{
|
||||
// ── 1. Missing tenant ────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void TryResolveTenantId_MissingTenant_ReturnsFalseWithTenantMissing()
|
||||
{
|
||||
// Arrange: bare context with no claims, no headers
|
||||
var context = new DefaultHttpContext();
|
||||
context.User = new ClaimsPrincipal(new ClaimsIdentity());
|
||||
|
||||
// Act
|
||||
var result = StellaOpsTenantResolver.TryResolveTenantId(context, out var tenantId, out var error);
|
||||
|
||||
// Assert
|
||||
result.Should().BeFalse("no tenant claim or header was provided");
|
||||
tenantId.Should().BeEmpty();
|
||||
error.Should().Be("tenant_missing");
|
||||
}
|
||||
|
||||
// ── 2. Canonical claim resolves ──────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void TryResolveTenantId_CanonicalClaim_ResolvesTenant()
|
||||
{
|
||||
// Arrange
|
||||
var context = new DefaultHttpContext();
|
||||
context.User = new ClaimsPrincipal(
|
||||
new ClaimsIdentity(
|
||||
new[] { new Claim(StellaOpsClaimTypes.Tenant, "concelier-tenant-a") },
|
||||
authenticationType: "test"));
|
||||
|
||||
// Act
|
||||
var result = StellaOpsTenantResolver.TryResolveTenantId(context, out var tenantId, out var error);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
tenantId.Should().Be("concelier-tenant-a");
|
||||
error.Should().BeNull();
|
||||
}
|
||||
|
||||
// ── 3. Legacy tid claim falls back ───────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void TryResolveTenantId_LegacyTidClaim_FallsBack()
|
||||
{
|
||||
// Arrange: only legacy "tid" claim present
|
||||
var context = new DefaultHttpContext();
|
||||
context.User = new ClaimsPrincipal(
|
||||
new ClaimsIdentity(
|
||||
new[] { new Claim("tid", "concelier-legacy-tenant") },
|
||||
authenticationType: "test"));
|
||||
|
||||
// Act
|
||||
var result = StellaOpsTenantResolver.TryResolveTenantId(context, out var tenantId, out var error);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
tenantId.Should().Be("concelier-legacy-tenant");
|
||||
error.Should().BeNull();
|
||||
}
|
||||
|
||||
// ── 4. Canonical header resolves ─────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void TryResolveTenantId_CanonicalHeader_ResolvesTenant()
|
||||
{
|
||||
// Arrange: no claims, only canonical header
|
||||
var context = new DefaultHttpContext();
|
||||
context.User = new ClaimsPrincipal(new ClaimsIdentity());
|
||||
context.Request.Headers[StellaOpsHttpHeaderNames.Tenant] = "concelier-header-tenant";
|
||||
|
||||
// Act
|
||||
var result = StellaOpsTenantResolver.TryResolveTenantId(context, out var tenantId, out var error);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
tenantId.Should().Be("concelier-header-tenant");
|
||||
error.Should().BeNull();
|
||||
}
|
||||
|
||||
// ── 5. Full context resolves actor and project ───────────────────────
|
||||
|
||||
[Fact]
|
||||
public void TryResolve_FullContext_ResolvesActorAndProject()
|
||||
{
|
||||
// Arrange: canonical tenant claim + sub claim for actor resolution
|
||||
var context = new DefaultHttpContext();
|
||||
context.User = new ClaimsPrincipal(
|
||||
new ClaimsIdentity(
|
||||
new[]
|
||||
{
|
||||
new Claim(StellaOpsClaimTypes.Tenant, "Concelier-Org-42"),
|
||||
new Claim(StellaOpsClaimTypes.Subject, "feed-sync-agent"),
|
||||
},
|
||||
authenticationType: "test"));
|
||||
|
||||
// Act
|
||||
var result = StellaOpsTenantResolver.TryResolve(context, out var tenantContext, out var error);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
error.Should().BeNull();
|
||||
tenantContext.Should().NotBeNull();
|
||||
tenantContext!.TenantId.Should().Be("concelier-org-42", "tenant IDs are normalised to lower-case");
|
||||
tenantContext.ActorId.Should().Be("feed-sync-agent");
|
||||
tenantContext.Source.Should().Be(TenantSource.Claim);
|
||||
}
|
||||
|
||||
// ── 6. Conflicting headers return tenant_conflict ────────────────────
|
||||
|
||||
[Fact]
|
||||
public void TryResolveTenantId_ConflictingHeaders_ReturnsTenantConflict()
|
||||
{
|
||||
// Arrange: canonical and legacy headers have different values
|
||||
var context = new DefaultHttpContext();
|
||||
context.User = new ClaimsPrincipal(new ClaimsIdentity());
|
||||
context.Request.Headers[StellaOpsHttpHeaderNames.Tenant] = "concelier-alpha";
|
||||
context.Request.Headers["X-Stella-Tenant"] = "concelier-beta";
|
||||
|
||||
// Act
|
||||
var result = StellaOpsTenantResolver.TryResolveTenantId(context, out var tenantId, out var error);
|
||||
|
||||
// Assert
|
||||
result.Should().BeFalse("conflicting headers should be rejected");
|
||||
error.Should().Be("tenant_conflict");
|
||||
}
|
||||
|
||||
// ── 7. Claim-header mismatch returns tenant_conflict ─────────────────
|
||||
|
||||
[Fact]
|
||||
public void TryResolveTenantId_ClaimHeaderMismatch_ReturnsTenantConflict()
|
||||
{
|
||||
// Arrange: claim says one tenant, header says another
|
||||
var context = new DefaultHttpContext();
|
||||
context.User = new ClaimsPrincipal(
|
||||
new ClaimsIdentity(
|
||||
new[] { new Claim(StellaOpsClaimTypes.Tenant, "concelier-from-claim") },
|
||||
authenticationType: "test"));
|
||||
context.Request.Headers[StellaOpsHttpHeaderNames.Tenant] = "concelier-from-header";
|
||||
|
||||
// Act
|
||||
var result = StellaOpsTenantResolver.TryResolveTenantId(context, out var tenantId, out var error);
|
||||
|
||||
// Assert
|
||||
result.Should().BeFalse("claim-header mismatch is a conflict");
|
||||
error.Should().Be("tenant_conflict");
|
||||
}
|
||||
|
||||
// ── 8. Matching claim and header is not a conflict ───────────────────
|
||||
|
||||
[Fact]
|
||||
public void TryResolveTenantId_MatchingClaimAndHeader_NoConflict()
|
||||
{
|
||||
// Arrange: claim and header agree on the same tenant value
|
||||
var context = new DefaultHttpContext();
|
||||
context.User = new ClaimsPrincipal(
|
||||
new ClaimsIdentity(
|
||||
new[] { new Claim(StellaOpsClaimTypes.Tenant, "concelier-same") },
|
||||
authenticationType: "test"));
|
||||
context.Request.Headers[StellaOpsHttpHeaderNames.Tenant] = "concelier-same";
|
||||
|
||||
// Act
|
||||
var result = StellaOpsTenantResolver.TryResolveTenantId(context, out var tenantId, out var error);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue("claim and header agree");
|
||||
tenantId.Should().Be("concelier-same");
|
||||
error.Should().BeNull();
|
||||
}
|
||||
}
|
||||
@@ -2085,6 +2085,9 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
|
||||
Environment.SetEnvironmentVariable("CONCELIER_SKIP_OPTIONS_VALIDATION", "1");
|
||||
Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", "Testing");
|
||||
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Testing");
|
||||
// Explicitly disable authority for these tests - they test endpoint logic without auth middleware.
|
||||
// Use correct single-underscore prefix that Program.cs Testing branch reads.
|
||||
Environment.SetEnvironmentVariable("CONCELIER_AUTHORITY__ENABLED", "false");
|
||||
const string EvidenceRootKey = "CONCELIER_EVIDENCE__ROOT";
|
||||
var repoRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "..", ".."));
|
||||
_additionalPreviousEnvironment[EvidenceRootKey] = Environment.GetEnvironmentVariable(EvidenceRootKey);
|
||||
|
||||
Reference in New Issue
Block a user