Add topology auth policies + journey findings notes
Concelier: - Register Topology.Read, Topology.Manage, Topology.Admin authorization policies mapped to OrchRead/OrchOperate/PlatformContextRead/IntegrationWrite scopes. Previously these policies were referenced by endpoints but never registered, causing System.InvalidOperationException on every topology API call. Gateway routes: - Simplified targets/environments routes (removed specific sub-path routes, use catch-all patterns instead) - Changed environments base route to JobEngine (where CRUD lives) - Changed to ReverseProxy type for all topology routes KNOWN ISSUE (not yet fixed): - ReverseProxy routes don't forward the gateway's identity envelope to Concelier. The regions/targets/bindings endpoints return 401 because hasPrincipal=False — the gateway authenticates the user but doesn't pass the identity to the backend via ReverseProxy. Microservice routes use Valkey transport which includes envelope headers. Topology endpoints need either: (a) Valkey transport registration in Concelier, or (b) Concelier configured to accept raw bearer tokens on ReverseProxy paths. This is an architecture-level fix. Journey findings collected so far: - Integration wizard (Harbor + GitHub App): works end-to-end - Advisory Check All: fixed (parallel individual checks) - Mirror domain creation: works, generate-immediately fails silently - Topology wizard Step 1 (Region): blocked by auth passthrough issue - Topology wizard Step 2 (Environment): POST to JobEngine needs verify - User ID resolution: raw hashes shown everywhere Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using StellaOps.Concelier.WebService.Options;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.WebService.Tests;
|
||||
|
||||
public sealed class ConcelierOptionsValidatorTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Trait("Intent", "Operational")]
|
||||
[Fact]
|
||||
public void Validate_AllowsEnabledMirrorWithoutStaticDomains()
|
||||
{
|
||||
var options = new ConcelierOptions
|
||||
{
|
||||
PostgresStorage = new ConcelierOptions.PostgresStorageOptions
|
||||
{
|
||||
Enabled = true,
|
||||
ConnectionString = "Host=postgres;Database=stellaops;Username=stellaops;Password=stellaops"
|
||||
},
|
||||
Mirror = new ConcelierOptions.MirrorOptions
|
||||
{
|
||||
Enabled = true,
|
||||
ExportRoot = "/var/lib/concelier/jobs/mirror-exports",
|
||||
ExportRootAbsolute = "/var/lib/concelier/jobs/mirror-exports",
|
||||
LatestDirectoryName = "latest",
|
||||
MirrorDirectoryName = "mirror"
|
||||
},
|
||||
Evidence = new ConcelierOptions.EvidenceBundleOptions
|
||||
{
|
||||
Root = "/var/lib/concelier/jobs/evidence-bundles",
|
||||
RootAbsolute = "/var/lib/concelier/jobs/evidence-bundles"
|
||||
}
|
||||
};
|
||||
|
||||
var exception = Record.Exception(() => ConcelierOptionsValidator.Validate(options));
|
||||
|
||||
Assert.Null(exception);
|
||||
}
|
||||
}
|
||||
@@ -1532,6 +1532,81 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
|
||||
Assert.True(limitedResponse.Headers.RetryAfter!.Delta!.Value.TotalSeconds > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MirrorManagementEndpointsUseAdvisorySourcesNamespaceAndGeneratePublicArtifacts()
|
||||
{
|
||||
using var temp = new TempDirectory();
|
||||
var environment = new Dictionary<string, string?>
|
||||
{
|
||||
["CONCELIER_MIRROR__ENABLED"] = "true",
|
||||
["CONCELIER_MIRROR__EXPORTROOT"] = temp.Path,
|
||||
["CONCELIER_MIRROR__ACTIVEEXPORTID"] = "latest",
|
||||
["CONCELIER_MIRROR__MAXINDEXREQUESTSPERHOUR"] = "5",
|
||||
["CONCELIER_MIRROR__MAXDOWNLOADREQUESTSPERHOUR"] = "5"
|
||||
};
|
||||
|
||||
using var factory = new ConcelierApplicationFactory(_runner.ConnectionString, environmentOverrides: environment);
|
||||
using var client = factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-Stella-Tenant", "test-tenant");
|
||||
|
||||
var configResponse = await client.GetAsync("/api/v1/advisory-sources/mirror/config");
|
||||
Assert.Equal(HttpStatusCode.OK, configResponse.StatusCode);
|
||||
|
||||
var createResponse = await client.PostAsJsonAsync(
|
||||
"/api/v1/advisory-sources/mirror/domains",
|
||||
new
|
||||
{
|
||||
domainId = "primary",
|
||||
displayName = "Primary",
|
||||
sourceIds = new[] { "nvd", "osv" },
|
||||
exportFormat = "JSON",
|
||||
rateLimits = new
|
||||
{
|
||||
indexRequestsPerHour = 60,
|
||||
downloadRequestsPerHour = 120
|
||||
},
|
||||
requireAuthentication = false,
|
||||
signing = new
|
||||
{
|
||||
enabled = false,
|
||||
algorithm = "HMAC-SHA256",
|
||||
keyId = string.Empty
|
||||
}
|
||||
});
|
||||
Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode);
|
||||
|
||||
var generateResponse = await client.PostAsync("/api/v1/advisory-sources/mirror/domains/primary/generate", content: null);
|
||||
Assert.Equal(HttpStatusCode.Accepted, generateResponse.StatusCode);
|
||||
|
||||
var endpointsResponse = await client.GetAsync("/api/v1/advisory-sources/mirror/domains/primary/endpoints");
|
||||
Assert.Equal(HttpStatusCode.OK, endpointsResponse.StatusCode);
|
||||
var endpointsJson = JsonDocument.Parse(await endpointsResponse.Content.ReadAsStringAsync());
|
||||
var endpoints = endpointsJson.RootElement.GetProperty("endpoints").EnumerateArray().Select(element => element.GetProperty("path").GetString()).ToList();
|
||||
Assert.Contains("/concelier/exports/index.json", endpoints);
|
||||
Assert.Contains("/concelier/exports/mirror/primary/manifest.json", endpoints);
|
||||
Assert.Contains("/concelier/exports/mirror/primary/bundle.json", endpoints);
|
||||
|
||||
var statusResponse = await client.GetAsync("/api/v1/advisory-sources/mirror/domains/primary/status");
|
||||
Assert.Equal(HttpStatusCode.OK, statusResponse.StatusCode);
|
||||
var statusJson = JsonDocument.Parse(await statusResponse.Content.ReadAsStringAsync());
|
||||
Assert.Equal("fresh", statusJson.RootElement.GetProperty("staleness").GetString());
|
||||
|
||||
var indexResponse = await client.GetAsync("/concelier/exports/index.json");
|
||||
Assert.Equal(HttpStatusCode.OK, indexResponse.StatusCode);
|
||||
var indexContent = await indexResponse.Content.ReadAsStringAsync();
|
||||
Assert.Contains(@"""domainId"":""primary""", indexContent, StringComparison.Ordinal);
|
||||
|
||||
var manifestResponse = await client.GetAsync("/concelier/exports/mirror/primary/manifest.json");
|
||||
Assert.Equal(HttpStatusCode.OK, manifestResponse.StatusCode);
|
||||
var manifestContent = await manifestResponse.Content.ReadAsStringAsync();
|
||||
Assert.Contains(@"""domainId"":""primary""", manifestContent, StringComparison.Ordinal);
|
||||
|
||||
var bundleResponse = await client.GetAsync("/concelier/exports/mirror/primary/bundle.json");
|
||||
Assert.Equal(HttpStatusCode.OK, bundleResponse.StatusCode);
|
||||
var bundleContent = await bundleResponse.Content.ReadAsStringAsync();
|
||||
Assert.Contains(@"""domainId"":""primary""", bundleContent, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MergeModuleDisabledByDefault()
|
||||
{
|
||||
@@ -4002,4 +4077,3 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user