Follow-up to SPRINT_20260419_027_Concelier_durable_affected_symbol_runtime. UnsupportedRuntimeWiringTests updated for the removed non-testing UnsupportedAffectedSymbol registration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
608 lines
26 KiB
C#
608 lines
26 KiB
C#
using FluentAssertions;
|
|
using Microsoft.AspNetCore.Authentication;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Hosting;
|
|
using Microsoft.AspNetCore.Mvc.Testing;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using StellaOps.Auth.Abstractions;
|
|
using StellaOps.Concelier.Core.Federation;
|
|
using StellaOps.Concelier.Core.Jobs;
|
|
using StellaOps.Concelier.Core.Observations;
|
|
using StellaOps.Concelier.Core.Orchestration;
|
|
using StellaOps.Concelier.Core.Raw;
|
|
using StellaOps.Concelier.Core.Signals;
|
|
using StellaOps.Concelier.Persistence.Postgres.Repositories;
|
|
using StellaOps.Replay.Core.FeedSnapshot;
|
|
using StellaOps.Concelier.WebService.Extensions;
|
|
using StellaOps.Concelier.WebService.Services;
|
|
using Moq;
|
|
using System.Net.Http.Json;
|
|
using System.Security.Cryptography;
|
|
using System.Security.Claims;
|
|
using System.Text.Json;
|
|
using System.Text.Encodings.Web;
|
|
using Xunit;
|
|
|
|
namespace StellaOps.Concelier.WebService.Tests;
|
|
|
|
public sealed class UnsupportedRuntimeWiringTests
|
|
{
|
|
[Fact]
|
|
public void DevelopmentHost_ResolvesUnsupportedRuntimeServices()
|
|
{
|
|
using var factory = new UnsupportedRuntimeWebApplicationFactory();
|
|
using var scope = factory.Services.CreateScope();
|
|
|
|
scope.ServiceProvider.GetRequiredService<IJobStore>().Should().BeOfType<UnsupportedJobStore>();
|
|
scope.ServiceProvider.GetRequiredService<IJobCoordinator>().Should().BeOfType<UnsupportedJobCoordinator>();
|
|
scope.ServiceProvider.GetRequiredService<IOrchestratorRegistryStore>().Should().BeOfType<UnsupportedOrchestratorRegistryStore>();
|
|
scope.ServiceProvider.GetRequiredService<IAffectedSymbolStore>().Should().BeOfType<PostgresAffectedSymbolStore>();
|
|
scope.ServiceProvider.GetRequiredService<IAffectedSymbolProvider>().Should().BeOfType<AffectedSymbolProvider>();
|
|
factory.HasJobSchedulerHostedService.Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DevelopmentHost_JobsEndpoint_ReturnsNotImplemented()
|
|
{
|
|
using var factory = new UnsupportedRuntimeWebApplicationFactory();
|
|
using var client = factory.CreateClient();
|
|
|
|
var response = await client.GetAsync("/jobs/definitions");
|
|
var body = await response.Content.ReadAsStringAsync();
|
|
|
|
response.StatusCode.Should().Be(System.Net.HttpStatusCode.NotImplemented);
|
|
body.Should().Contain("NOT_IMPLEMENTED");
|
|
body.Should().Contain("durable backend implementation");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DevelopmentHost_SourceSyncEndpoint_ReturnsNotImplemented()
|
|
{
|
|
using var factory = new UnsupportedRuntimeWebApplicationFactory();
|
|
using var client = factory.CreateClient();
|
|
using var request = new HttpRequestMessage(HttpMethod.Post, "/api/v1/advisory-sources/nvd/sync");
|
|
request.Headers.Add(Program.TenantHeaderName, "tenant-a");
|
|
|
|
var response = await client.SendAsync(request);
|
|
var body = await response.Content.ReadAsStringAsync();
|
|
|
|
response.StatusCode.Should().Be(System.Net.HttpStatusCode.NotImplemented);
|
|
body.Should().Contain("NOT_IMPLEMENTED");
|
|
body.Should().Contain("durable backend implementation");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DevelopmentHost_BatchSourceSyncEndpoint_ReturnsNotImplemented()
|
|
{
|
|
using var factory = new UnsupportedRuntimeWebApplicationFactory();
|
|
using var client = factory.CreateClient();
|
|
using var request = new HttpRequestMessage(HttpMethod.Post, "/api/v1/advisory-sources/sync");
|
|
request.Headers.Add(Program.TenantHeaderName, "tenant-a");
|
|
|
|
var response = await client.SendAsync(request);
|
|
var body = await response.Content.ReadAsStringAsync();
|
|
|
|
response.StatusCode.Should().Be(System.Net.HttpStatusCode.NotImplemented);
|
|
body.Should().Contain("NOT_IMPLEMENTED");
|
|
body.Should().Contain("durable backend implementation");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DevelopmentHost_InternalOrchestratorEndpoint_ReturnsNotImplemented()
|
|
{
|
|
using var factory = new UnsupportedRuntimeWebApplicationFactory();
|
|
using var client = factory.CreateClient();
|
|
using var request = new HttpRequestMessage(
|
|
HttpMethod.Get,
|
|
$"/internal/orch/commands?connectorId=nvd&runId={Guid.NewGuid()}");
|
|
request.Headers.Add(Program.TenantHeaderName, "tenant-a");
|
|
|
|
var response = await client.SendAsync(request);
|
|
var body = await response.Content.ReadAsStringAsync();
|
|
|
|
response.StatusCode.Should().Be(System.Net.HttpStatusCode.NotImplemented);
|
|
body.Should().Contain("NOT_IMPLEMENTED");
|
|
body.Should().Contain("orchestrator registry");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DevelopmentHost_ConfigSeededMirrorEndpoints_IgnoreConfiguredDomains()
|
|
{
|
|
using var temp = new TemporaryDirectory();
|
|
var exportId = "20251019T120000Z";
|
|
var domainRoot = Path.Combine(temp.Path, exportId, "mirror", "primary");
|
|
Directory.CreateDirectory(domainRoot);
|
|
|
|
await File.WriteAllTextAsync(
|
|
Path.Combine(temp.Path, exportId, "mirror", "index.json"),
|
|
"""{"schemaVersion":1,"domains":[{"id":"primary"}]}""");
|
|
await File.WriteAllTextAsync(
|
|
Path.Combine(domainRoot, "manifest.json"),
|
|
"""{"domainId":"primary"}""");
|
|
|
|
using var factory = new UnsupportedRuntimeWebApplicationFactory(new Dictionary<string, string?>
|
|
{
|
|
["CONCELIER_MIRROR__ENABLED"] = "true",
|
|
["CONCELIER_MIRROR__EXPORTROOT"] = temp.Path,
|
|
["CONCELIER_MIRROR__ACTIVEEXPORTID"] = exportId,
|
|
["CONCELIER_MIRROR__DOMAINS__0__ID"] = "primary",
|
|
["CONCELIER_MIRROR__DOMAINS__0__REQUIREAUTHENTICATION"] = "false",
|
|
["CONCELIER_MIRROR__MAXINDEXREQUESTSPERHOUR"] = "5",
|
|
});
|
|
using var client = factory.CreateClient();
|
|
|
|
using var indexRequest = new HttpRequestMessage(HttpMethod.Get, "/concelier/exports/index.json");
|
|
indexRequest.Headers.Add(Program.TenantHeaderName, "tenant-a");
|
|
var indexResponse = await client.SendAsync(indexRequest);
|
|
|
|
using var manifestRequest = new HttpRequestMessage(HttpMethod.Get, "/concelier/exports/mirror/primary/manifest.json");
|
|
manifestRequest.Headers.Add(Program.TenantHeaderName, "tenant-a");
|
|
var manifestResponse = await client.SendAsync(manifestRequest);
|
|
|
|
indexResponse.StatusCode.Should().Be(System.Net.HttpStatusCode.NotFound);
|
|
manifestResponse.StatusCode.Should().Be(System.Net.HttpStatusCode.NotFound);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DevelopmentHost_MirrorImportEndpoints_ImportBundleAndExposeMirrorArtifacts()
|
|
{
|
|
using var source = new TemporaryDirectory();
|
|
using var exportRoot = new TemporaryDirectory();
|
|
var exportId = "mirror-import-proof";
|
|
var bundleJson = """
|
|
{
|
|
"schemaVersion": 1,
|
|
"generatedAt": "2026-04-18T00:00:00Z",
|
|
"domainId": "primary",
|
|
"displayName": "Primary",
|
|
"exportFormat": "JSON",
|
|
"sourceIds": ["nvd"],
|
|
"advisories": [
|
|
{
|
|
"id": "CVE-2026-0001"
|
|
}
|
|
]
|
|
}
|
|
""";
|
|
var bundlePath = Path.Combine(source.Path, "bundle.json");
|
|
await File.WriteAllTextAsync(bundlePath, bundleJson);
|
|
|
|
var bundleDigest = Convert.ToHexString(SHA256.HashData(await File.ReadAllBytesAsync(bundlePath))).ToLowerInvariant();
|
|
var manifestJson = $$"""
|
|
{
|
|
"domainId": "primary",
|
|
"displayName": "Primary",
|
|
"generatedAt": "2026-04-18T00:00:00Z",
|
|
"exports": [
|
|
{
|
|
"key": "primary",
|
|
"exportId": "primary",
|
|
"format": "JSON",
|
|
"artifactSizeBytes": {{new FileInfo(bundlePath).Length}},
|
|
"artifactDigest": "sha256:{{bundleDigest}}"
|
|
}
|
|
]
|
|
}
|
|
""";
|
|
await File.WriteAllTextAsync(Path.Combine(source.Path, "manifest.json"), manifestJson);
|
|
|
|
using var factory = new UnsupportedRuntimeWebApplicationFactory(new Dictionary<string, string?>
|
|
{
|
|
["CONCELIER_MIRROR__ENABLED"] = "true",
|
|
["CONCELIER_MIRROR__EXPORTROOT"] = exportRoot.Path,
|
|
["CONCELIER_MIRROR__IMPORTROOT"] = source.Path,
|
|
["CONCELIER_MIRROR__ACTIVEEXPORTID"] = exportId,
|
|
});
|
|
using var client = factory.CreateClient();
|
|
|
|
using var importRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v1/advisory-sources/mirror/import")
|
|
{
|
|
Content = JsonContent.Create(new { bundlePath = source.Path })
|
|
};
|
|
importRequest.Headers.Add(Program.TenantHeaderName, "tenant-a");
|
|
|
|
var importResponse = await client.SendAsync(importRequest);
|
|
var importBody = await importResponse.Content.ReadAsStringAsync();
|
|
|
|
using var statusRequest = new HttpRequestMessage(HttpMethod.Get, "/api/v1/advisory-sources/mirror/import/status");
|
|
statusRequest.Headers.Add(Program.TenantHeaderName, "tenant-a");
|
|
|
|
var statusResponse = await client.SendAsync(statusRequest);
|
|
var statusBody = await statusResponse.Content.ReadAsStringAsync();
|
|
|
|
using var manifestRequest = new HttpRequestMessage(HttpMethod.Get, "/concelier/exports/mirror/primary/manifest.json");
|
|
manifestRequest.Headers.Add(Program.TenantHeaderName, "tenant-a");
|
|
var manifestResponse = await client.SendAsync(manifestRequest);
|
|
var manifestBody = await manifestResponse.Content.ReadAsStringAsync();
|
|
|
|
importResponse.StatusCode.Should().Be(System.Net.HttpStatusCode.OK, importBody);
|
|
importBody.Should().Contain("completed");
|
|
statusResponse.StatusCode.Should().Be(System.Net.HttpStatusCode.OK, statusBody);
|
|
statusBody.Should().Contain("\"hasImport\":true");
|
|
statusBody.Should().Contain("\"status\":\"completed\"");
|
|
statusBody.Should().Contain("\"domainId\":\"primary\"");
|
|
statusBody.Should().Contain("Detached JWS signature was not found");
|
|
manifestResponse.StatusCode.Should().Be(System.Net.HttpStatusCode.OK, manifestBody);
|
|
|
|
using var manifestDocument = JsonDocument.Parse(manifestBody);
|
|
manifestDocument.RootElement.GetProperty("domainId").GetString().Should().Be("primary");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DevelopmentHost_MirrorImportEndpoints_RejectChecksumMismatch()
|
|
{
|
|
using var source = new TemporaryDirectory();
|
|
using var exportRoot = new TemporaryDirectory();
|
|
|
|
var bundlePath = Path.Combine(source.Path, "bundle.json");
|
|
await File.WriteAllTextAsync(
|
|
bundlePath,
|
|
"""
|
|
{
|
|
"schemaVersion": 1,
|
|
"generatedAt": "2026-04-18T00:00:00Z",
|
|
"domainId": "primary",
|
|
"displayName": "Primary",
|
|
"exportFormat": "JSON",
|
|
"sourceIds": ["nvd"],
|
|
"advisories": []
|
|
}
|
|
""");
|
|
var bundleLength = new FileInfo(bundlePath).Length;
|
|
await File.WriteAllTextAsync(
|
|
Path.Combine(source.Path, "manifest.json"),
|
|
$$"""
|
|
{
|
|
"domainId": "primary",
|
|
"displayName": "Primary",
|
|
"generatedAt": "2026-04-18T00:00:00Z",
|
|
"exports": [
|
|
{
|
|
"key": "primary",
|
|
"exportId": "primary",
|
|
"format": "JSON",
|
|
"artifactSizeBytes": {{bundleLength}},
|
|
"artifactDigest": "sha256:deadbeef"
|
|
}
|
|
]
|
|
}
|
|
""");
|
|
|
|
using var factory = new UnsupportedRuntimeWebApplicationFactory(new Dictionary<string, string?>
|
|
{
|
|
["CONCELIER_MIRROR__ENABLED"] = "true",
|
|
["CONCELIER_MIRROR__EXPORTROOT"] = exportRoot.Path,
|
|
["CONCELIER_MIRROR__IMPORTROOT"] = source.Path,
|
|
["CONCELIER_MIRROR__ACTIVEEXPORTID"] = "mirror-import-failure",
|
|
});
|
|
using var client = factory.CreateClient();
|
|
|
|
using var importRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v1/advisory-sources/mirror/import")
|
|
{
|
|
Content = JsonContent.Create(new { bundlePath = source.Path })
|
|
};
|
|
importRequest.Headers.Add(Program.TenantHeaderName, "tenant-a");
|
|
|
|
var importResponse = await client.SendAsync(importRequest);
|
|
var importBody = await importResponse.Content.ReadAsStringAsync();
|
|
|
|
using var statusRequest = new HttpRequestMessage(HttpMethod.Get, "/api/v1/advisory-sources/mirror/import/status");
|
|
statusRequest.Headers.Add(Program.TenantHeaderName, "tenant-a");
|
|
var statusResponse = await client.SendAsync(statusRequest);
|
|
var statusBody = await statusResponse.Content.ReadAsStringAsync();
|
|
|
|
importResponse.StatusCode.Should().Be(System.Net.HttpStatusCode.BadRequest, importBody);
|
|
importBody.Should().Contain("\"status\":\"failed\"");
|
|
importBody.Should().Contain("digest mismatch");
|
|
statusResponse.StatusCode.Should().Be(System.Net.HttpStatusCode.OK, statusBody);
|
|
statusBody.Should().Contain("\"status\":\"failed\"");
|
|
statusBody.Should().Contain("digest mismatch");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DevelopmentHost_MirrorImportEndpoints_RejectPathsOutsideAllowlistedImportRoot()
|
|
{
|
|
using var source = new TemporaryDirectory();
|
|
using var importRoot = new TemporaryDirectory();
|
|
using var exportRoot = new TemporaryDirectory();
|
|
|
|
await File.WriteAllTextAsync(
|
|
Path.Combine(source.Path, "bundle.json"),
|
|
"""
|
|
{
|
|
"schemaVersion": 1,
|
|
"generatedAt": "2026-04-18T00:00:00Z",
|
|
"domainId": "primary",
|
|
"displayName": "Primary",
|
|
"exportFormat": "JSON",
|
|
"sourceIds": ["nvd"],
|
|
"advisories": []
|
|
}
|
|
""");
|
|
await File.WriteAllTextAsync(
|
|
Path.Combine(source.Path, "manifest.json"),
|
|
"""
|
|
{
|
|
"domainId": "primary",
|
|
"displayName": "Primary",
|
|
"generatedAt": "2026-04-18T00:00:00Z",
|
|
"exports": [
|
|
{
|
|
"key": "primary",
|
|
"exportId": "primary",
|
|
"format": "JSON",
|
|
"artifactSizeBytes": 120,
|
|
"artifactDigest": "sha256:deadbeef"
|
|
}
|
|
]
|
|
}
|
|
""");
|
|
|
|
using var factory = new UnsupportedRuntimeWebApplicationFactory(new Dictionary<string, string?>
|
|
{
|
|
["CONCELIER_MIRROR__ENABLED"] = "true",
|
|
["CONCELIER_MIRROR__EXPORTROOT"] = exportRoot.Path,
|
|
["CONCELIER_MIRROR__IMPORTROOT"] = importRoot.Path,
|
|
["CONCELIER_MIRROR__ACTIVEEXPORTID"] = "mirror-import-allowlist",
|
|
});
|
|
using var client = factory.CreateClient();
|
|
|
|
using var importRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v1/advisory-sources/mirror/import")
|
|
{
|
|
Content = JsonContent.Create(new { bundlePath = source.Path })
|
|
};
|
|
importRequest.Headers.Add(Program.TenantHeaderName, "tenant-a");
|
|
|
|
var importResponse = await client.SendAsync(importRequest);
|
|
var importBody = await importResponse.Content.ReadAsStringAsync();
|
|
|
|
importResponse.StatusCode.Should().Be(System.Net.HttpStatusCode.BadRequest, importBody);
|
|
importBody.Should().Contain("\"status\":\"failed\"");
|
|
importBody.Should().Contain("must stay within the configured mirror import root");
|
|
}
|
|
|
|
private sealed class UnsupportedRuntimeWebApplicationFactory : WebApplicationFactory<Program>
|
|
{
|
|
private readonly Dictionary<string, string?> _previousEnvironment = new(StringComparer.OrdinalIgnoreCase);
|
|
|
|
public bool HasJobSchedulerHostedService { get; private set; }
|
|
|
|
public UnsupportedRuntimeWebApplicationFactory(IReadOnlyDictionary<string, string?>? environmentOverrides = null)
|
|
{
|
|
SetEnvironmentVariable("CONCELIER_POSTGRESSTORAGE__CONNECTIONSTRING", "Host=localhost;Port=5432;Database=concelier_test;Username=postgres;Password=postgres");
|
|
SetEnvironmentVariable("CONCELIER_POSTGRESSTORAGE__ENABLED", "true");
|
|
SetEnvironmentVariable("CONCELIER_POSTGRESSTORAGE__COMMANDTIMEOUTSECONDS", "30");
|
|
SetEnvironmentVariable("CONCELIER_POSTGRESSTORAGE__SCHEMANAME", "vuln");
|
|
SetEnvironmentVariable("CONCELIER_POSTGRES_DSN", "Host=localhost;Port=5432;Database=concelier_test;Username=postgres;Password=postgres");
|
|
SetEnvironmentVariable("CONCELIER_SKIP_OPTIONS_VALIDATION", "1");
|
|
SetEnvironmentVariable("CONCELIER_TELEMETRY__ENABLED", "false");
|
|
SetEnvironmentVariable("CONCELIER_TELEMETRY__ENABLELOGGING", "false");
|
|
SetEnvironmentVariable("CONCELIER_TELEMETRY__ENABLETRACING", "false");
|
|
SetEnvironmentVariable("CONCELIER_TELEMETRY__ENABLEMETRICS", "false");
|
|
SetEnvironmentVariable("CONCELIER_AUTHORITY__ENABLED", "false");
|
|
SetEnvironmentVariable("CONCELIER_AUTHORITY__REQUIREDSCOPES__0", "concelier.jobs.trigger");
|
|
SetEnvironmentVariable("DOTNET_ENVIRONMENT", "Production");
|
|
SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Production");
|
|
|
|
if (environmentOverrides is not null)
|
|
{
|
|
foreach (var entry in environmentOverrides)
|
|
{
|
|
SetEnvironmentVariable(entry.Key, entry.Value);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
|
{
|
|
builder.UseEnvironment("Production");
|
|
builder.ConfigureServices(services =>
|
|
{
|
|
HasJobSchedulerHostedService = services.Any(descriptor =>
|
|
descriptor.ServiceType == typeof(IHostedService) &&
|
|
descriptor.ImplementationType == typeof(JobSchedulerHostedService));
|
|
|
|
services.AddHttpContextAccessor();
|
|
services.AddAuthentication("Test")
|
|
.AddScheme<AuthenticationSchemeOptions, AlwaysAllowAuthHandler>("Test", _ => { })
|
|
.AddScheme<AuthenticationSchemeOptions, AlwaysAllowAuthHandler>("StellaOpsBearer", _ => { });
|
|
services.AddAuthorization(options =>
|
|
{
|
|
options.AddPolicy("Concelier.Sources.Manage", policy => policy.RequireAssertion(_ => true));
|
|
options.AddPolicy("Concelier.Advisories.Read", policy => policy.RequireAssertion(_ => true));
|
|
});
|
|
services.TryAddSingleton<Func<Guid>>(_ => () => Guid.Parse("11111111-1111-1111-1111-111111111111"));
|
|
services.RemoveAll<IAdvisoryRawService>();
|
|
services.RemoveAll<IAdvisoryObservationQueryService>();
|
|
services.RemoveAll<ISnapshotIngestionOrchestrator>();
|
|
services.TryAddSingleton(Mock.Of<IAdvisoryRawRepository>());
|
|
services.TryAddSingleton(Mock.Of<IAdvisoryObservationLookup>());
|
|
services.TryAddSingleton(Mock.Of<IAdvisoryObservationSink>());
|
|
services.TryAddSingleton(CreateFeedSnapshotCoordinator());
|
|
services.RemoveAll<IMirrorDomainStore>();
|
|
services.AddSingleton<IMirrorDomainStore, InMemoryMirrorDomainStore>();
|
|
services.RemoveAll<IMirrorBundleImportStore>();
|
|
services.AddSingleton<IMirrorBundleImportStore, InMemoryMirrorBundleImportStore>();
|
|
services.RemoveAll<IHostedService>();
|
|
});
|
|
}
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
if (disposing)
|
|
{
|
|
foreach (var entry in _previousEnvironment)
|
|
{
|
|
Environment.SetEnvironmentVariable(entry.Key, entry.Value);
|
|
}
|
|
}
|
|
|
|
base.Dispose(disposing);
|
|
}
|
|
|
|
private void SetEnvironmentVariable(string name, string? value)
|
|
{
|
|
_previousEnvironment[name] = Environment.GetEnvironmentVariable(name);
|
|
Environment.SetEnvironmentVariable(name, value);
|
|
}
|
|
|
|
private static IFeedSnapshotCoordinator CreateFeedSnapshotCoordinator()
|
|
{
|
|
var mock = new Mock<IFeedSnapshotCoordinator>();
|
|
mock.SetupGet(x => x.RegisteredSources).Returns(Array.Empty<string>());
|
|
return mock.Object;
|
|
}
|
|
}
|
|
|
|
private sealed class InMemoryMirrorDomainStore : IMirrorDomainStore
|
|
{
|
|
private readonly object _sync = new();
|
|
private readonly Dictionary<string, MirrorDomainRecord> _domains = new(StringComparer.OrdinalIgnoreCase);
|
|
|
|
public IReadOnlyList<MirrorDomainRecord> GetAllDomains()
|
|
{
|
|
lock (_sync)
|
|
{
|
|
return _domains.Values.Select(Clone).ToArray();
|
|
}
|
|
}
|
|
|
|
public MirrorDomainRecord? GetDomain(string domainId)
|
|
{
|
|
lock (_sync)
|
|
{
|
|
return _domains.TryGetValue(domainId, out var domain) ? Clone(domain) : null;
|
|
}
|
|
}
|
|
|
|
public Task SaveDomainAsync(MirrorDomainRecord domain, CancellationToken ct = default)
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
lock (_sync)
|
|
{
|
|
_domains[domain.Id] = Clone(domain);
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task DeleteDomainAsync(string domainId, CancellationToken ct = default)
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
lock (_sync)
|
|
{
|
|
_domains.Remove(domainId);
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
private static MirrorDomainRecord Clone(MirrorDomainRecord domain) => new()
|
|
{
|
|
Id = domain.Id,
|
|
DisplayName = domain.DisplayName,
|
|
SourceIds = [.. domain.SourceIds],
|
|
ExportFormat = domain.ExportFormat,
|
|
RequireAuthentication = domain.RequireAuthentication,
|
|
MaxIndexRequestsPerHour = domain.MaxIndexRequestsPerHour,
|
|
MaxDownloadRequestsPerHour = domain.MaxDownloadRequestsPerHour,
|
|
SigningEnabled = domain.SigningEnabled,
|
|
SigningAlgorithm = domain.SigningAlgorithm,
|
|
SigningKeyId = domain.SigningKeyId,
|
|
Exports = domain.Exports
|
|
.Select(export => new MirrorExportRecord
|
|
{
|
|
Key = export.Key,
|
|
Format = export.Format,
|
|
Filters = new Dictionary<string, string>(export.Filters, StringComparer.Ordinal),
|
|
})
|
|
.ToList(),
|
|
CreatedAt = domain.CreatedAt,
|
|
UpdatedAt = domain.UpdatedAt,
|
|
LastGeneratedAt = domain.LastGeneratedAt,
|
|
LastGenerateTriggeredAt = domain.LastGenerateTriggeredAt,
|
|
BundleSizeBytes = domain.BundleSizeBytes,
|
|
AdvisoryCount = domain.AdvisoryCount,
|
|
};
|
|
}
|
|
|
|
private sealed class InMemoryMirrorBundleImportStore : IMirrorBundleImportStore
|
|
{
|
|
private MirrorImportStatusRecord? _status;
|
|
|
|
public MirrorImportStatusRecord? GetLatestStatus() => _status;
|
|
|
|
public void SetStatus(MirrorImportStatusRecord status) => _status = status;
|
|
}
|
|
|
|
private sealed class TemporaryDirectory : IDisposable
|
|
{
|
|
public TemporaryDirectory()
|
|
{
|
|
Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "concelier-mirror-" + Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(Path);
|
|
}
|
|
|
|
public string Path { get; }
|
|
|
|
public void Dispose()
|
|
{
|
|
try
|
|
{
|
|
if (Directory.Exists(Path))
|
|
{
|
|
Directory.Delete(Path, recursive: true);
|
|
}
|
|
}
|
|
catch (IOException)
|
|
{
|
|
}
|
|
catch (UnauthorizedAccessException)
|
|
{
|
|
}
|
|
}
|
|
}
|
|
|
|
private sealed class AlwaysAllowAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
|
|
{
|
|
public AlwaysAllowAuthHandler(
|
|
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
|
ILoggerFactory logger,
|
|
UrlEncoder encoder)
|
|
: base(options, logger, encoder)
|
|
{
|
|
}
|
|
|
|
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
|
{
|
|
var scopes = new[]
|
|
{
|
|
StellaOpsScopes.ConcelierJobsTrigger,
|
|
StellaOpsScopes.AdvisoryRead,
|
|
StellaOpsScopes.IntegrationWrite,
|
|
StellaOpsScopes.IntegrationOperate,
|
|
};
|
|
|
|
var identity = new ClaimsIdentity(
|
|
[
|
|
new Claim(ClaimTypes.NameIdentifier, "unsupported-runtime"),
|
|
new Claim(ClaimTypes.Name, "unsupported-runtime"),
|
|
new Claim(StellaOpsClaimTypes.Tenant, "tenant-a"),
|
|
new Claim(StellaOpsClaimTypes.Scope, string.Join(' ', scopes)),
|
|
new Claim(StellaOpsClaimTypes.ScopeItem, StellaOpsScopes.ConcelierJobsTrigger),
|
|
new Claim(StellaOpsClaimTypes.ScopeItem, StellaOpsScopes.AdvisoryRead),
|
|
new Claim(StellaOpsClaimTypes.ScopeItem, StellaOpsScopes.IntegrationWrite),
|
|
new Claim(StellaOpsClaimTypes.ScopeItem, StellaOpsScopes.IntegrationOperate),
|
|
], Scheme.Name);
|
|
|
|
var principal = new ClaimsPrincipal(identity);
|
|
var ticket = new AuthenticationTicket(principal, Scheme.Name);
|
|
return Task.FromResult(AuthenticateResult.Success(ticket));
|
|
}
|
|
}
|
|
}
|