work
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-25 08:01:23 +02:00
parent d92973d6fd
commit 6bee1fdcf5
207 changed files with 12816 additions and 2295 deletions

View File

@@ -1,41 +1,770 @@
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast");
app.Run();
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
using System.Text.Json.Serialization;
using StellaOps.PacksRegistry.Core.Contracts;
using StellaOps.PacksRegistry.Core.Models;
using StellaOps.PacksRegistry.Core.Services;
using StellaOps.PacksRegistry.Infrastructure.FileSystem;
using StellaOps.PacksRegistry.Infrastructure.InMemory;
using StellaOps.PacksRegistry.Infrastructure.Verification;
using StellaOps.PacksRegistry.Infrastructure.Mongo;
using StellaOps.PacksRegistry.Infrastructure.Options;
using StellaOps.PacksRegistry.WebService;
using StellaOps.PacksRegistry.WebService.Contracts;
using StellaOps.PacksRegistry.WebService.Options;
using Microsoft.Extensions.FileProviders;
using MongoDB.Driver;
var builder = WebApplication.CreateBuilder(args);
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
options.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
});
builder.Services.AddOpenApi();
var dataDir = builder.Configuration.GetValue<string>("PacksRegistry:DataDir");
var mongoOptions = builder.Configuration.GetSection("PacksRegistry:Mongo").Get<MongoOptions>() ?? new MongoOptions();
mongoOptions.ConnectionString ??= builder.Configuration.GetConnectionString("packs-registry");
if (!string.IsNullOrWhiteSpace(mongoOptions.ConnectionString))
{
builder.Services.AddSingleton(mongoOptions);
builder.Services.AddSingleton<IMongoClient>(_ => new MongoClient(mongoOptions.ConnectionString));
builder.Services.AddSingleton(sp => sp.GetRequiredService<IMongoClient>().GetDatabase(mongoOptions.Database));
builder.Services.AddSingleton<IPackRepository, MongoPackRepository>();
builder.Services.AddSingleton<IParityRepository, MongoParityRepository>();
builder.Services.AddSingleton<ILifecycleRepository, MongoLifecycleRepository>();
builder.Services.AddSingleton<IAuditRepository, MongoAuditRepository>();
builder.Services.AddSingleton<IAttestationRepository, MongoAttestationRepository>();
builder.Services.AddSingleton<IMirrorRepository, MongoMirrorRepository>();
builder.Services.AddHostedService<PacksMongoInitializer>();
}
else
{
builder.Services.AddSingleton<IPackRepository>(_ => new FilePackRepository(dataDir ?? "data/packs"));
builder.Services.AddSingleton<IParityRepository>(_ => new FileParityRepository(dataDir ?? "data/packs"));
builder.Services.AddSingleton<ILifecycleRepository>(_ => new FileLifecycleRepository(dataDir ?? "data/packs"));
builder.Services.AddSingleton<IAuditRepository>(_ => new FileAuditRepository(dataDir ?? "data/packs"));
builder.Services.AddSingleton<IAttestationRepository>(_ => new FileAttestationRepository(dataDir ?? "data/packs"));
builder.Services.AddSingleton<IMirrorRepository>(_ => new FileMirrorRepository(dataDir ?? "data/packs"));
}
var verificationSection = builder.Configuration.GetSection("PacksRegistry:Verification");
builder.Services.Configure<VerificationOptions>(verificationSection);
var publicKeyPem = verificationSection.GetValue<string>("PublicKeyPem");
if (!string.IsNullOrWhiteSpace(publicKeyPem))
{
builder.Services.AddSingleton<IPackSignatureVerifier>(_ => new RsaSignatureVerifier(publicKeyPem));
}
else
{
builder.Services.AddSingleton<IPackSignatureVerifier, SimpleSignatureVerifier>();
}
var authOptions = builder.Configuration.GetSection("PacksRegistry:Auth").Get<AuthOptions>() ?? new AuthOptions();
builder.Services.AddSingleton(authOptions);
var policyOptions = builder.Configuration.GetSection("PacksRegistry:Policy").Get<PackPolicyOptions>() ?? new PackPolicyOptions();
builder.Services.AddSingleton(policyOptions);
builder.Services.AddSingleton<PackService>();
builder.Services.AddSingleton<ParityService>();
builder.Services.AddSingleton<LifecycleService>();
builder.Services.AddSingleton<AttestationService>();
builder.Services.AddSingleton<MirrorService>();
builder.Services.AddSingleton<ComplianceService>();
builder.Services.AddSingleton<ExportService>();
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddHealthChecks();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.MapHealthChecks("/healthz");
// Serve static OpenAPI stubs for packs APIs (until unified spec is generated)
var openApiDir = Path.Combine(app.Environment.ContentRootPath, "OpenApi");
if (Directory.Exists(openApiDir))
{
var provider = new PhysicalFileProvider(openApiDir);
app.MapGet("/openapi/packs.json", () =>
{
var file = provider.GetFileInfo("packs.openapi.json");
return file.Exists
? Results.File(file.CreateReadStream(), "application/json")
: Results.NotFound();
})
.ExcludeFromDescription();
app.MapGet("/openapi/pack-manifest.json", () =>
{
var file = provider.GetFileInfo("pack-manifest.openapi.json");
return file.Exists
? Results.File(file.CreateReadStream(), "application/json")
: Results.NotFound();
})
.ExcludeFromDescription();
}
app.MapPost("/api/v1/packs", async (PackUploadRequest request, PackService service, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) =>
{
if (!IsAuthorized(context, auth, out var unauthorizedResult))
{
return unauthorizedResult;
}
var tenantHeader = context.Request.Headers["X-StellaOps-Tenant"].ToString();
var tenant = !string.IsNullOrWhiteSpace(request.TenantId) ? request.TenantId : tenantHeader;
if (string.IsNullOrWhiteSpace(tenant))
{
return Results.BadRequest(new { error = "tenant_missing", message = "X-StellaOps-Tenant header or tenantId is required." });
}
if (!IsTenantAllowed(tenant, auth, out var tenantResult))
{
return tenantResult;
}
if (request.Content == null || request.Content.Length == 0)
{
return Results.BadRequest(new { error = "content_missing", message = "Content (base64) is required." });
}
try
{
var contentBytes = Convert.FromBase64String(request.Content);
byte[]? provenanceBytes = null;
if (!string.IsNullOrWhiteSpace(request.ProvenanceContent))
{
provenanceBytes = Convert.FromBase64String(request.ProvenanceContent);
}
var record = await service.UploadAsync(
name: request.Name ?? string.Empty,
version: request.Version ?? string.Empty,
tenantId: tenant,
content: contentBytes,
signature: request.Signature,
provenanceUri: request.ProvenanceUri,
provenanceContent: provenanceBytes,
metadata: request.Metadata,
cancellationToken: cancellationToken);
return Results.Created($"/api/v1/packs/{record.PackId}", PackResponse.From(record));
}
catch (FormatException)
{
return Results.BadRequest(new { error = "content_base64_invalid", message = "Content must be valid base64." });
}
catch (Exception ex)
{
return Results.BadRequest(new { error = "upload_failed", message = ex.Message });
}
})
.Produces<PackResponse>(StatusCodes.Status201Created)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status401Unauthorized);
app.MapGet("/api/v1/packs", async (string? tenant, PackService service, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) =>
{
if (!IsAuthorized(context, auth, out var unauthorizedResult))
{
return unauthorizedResult;
}
var tenantHeader = context.Request.Headers["X-StellaOps-Tenant"].ToString();
var effectiveTenant = !string.IsNullOrWhiteSpace(tenant) ? tenant : tenantHeader;
if (auth.AllowedTenants is { Length: > 0 } && string.IsNullOrWhiteSpace(effectiveTenant))
{
return Results.BadRequest(new { error = "tenant_missing", message = "tenant query parameter or X-StellaOps-Tenant header is required when tenant allowlists are configured." });
}
if (!string.IsNullOrWhiteSpace(effectiveTenant) && !IsTenantAllowed(effectiveTenant, auth, out var tenantResult))
{
return tenantResult;
}
var packs = await service.ListAsync(effectiveTenant, cancellationToken).ConfigureAwait(false);
return Results.Ok(packs.Select(PackResponse.From));
})
.Produces<IEnumerable<PackResponse>>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized);
app.MapGet("/api/v1/packs/{packId}", async (string packId, PackService service, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) =>
{
if (!IsAuthorized(context, auth, out var unauthorizedResult))
{
return unauthorizedResult;
}
var tenantHeader = context.Request.Headers["X-StellaOps-Tenant"].ToString();
var record = await service.GetAsync(packId, cancellationToken).ConfigureAwait(false);
if (record is null)
{
return Results.NotFound();
}
if (!IsTenantAllowed(record.TenantId, auth, out var tenantResult) ||
(!string.IsNullOrWhiteSpace(tenantHeader) && !string.Equals(record.TenantId, tenantHeader, StringComparison.OrdinalIgnoreCase)))
{
return tenantResult ?? Results.Forbid();
}
return Results.Ok(PackResponse.From(record));
})
.Produces<PackResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status404NotFound);
app.MapGet("/api/v1/packs/{packId}/content", async (string packId, PackService service, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) =>
{
if (!IsAuthorized(context, auth, out var unauthorizedResult))
{
return unauthorizedResult;
}
var tenantHeader = context.Request.Headers["X-StellaOps-Tenant"].ToString();
var record = await service.GetAsync(packId, cancellationToken).ConfigureAwait(false);
if (record is null)
{
return Results.NotFound();
}
if (!IsTenantAllowed(record.TenantId, auth, out var tenantResult) ||
(!string.IsNullOrWhiteSpace(tenantHeader) && !string.Equals(record.TenantId, tenantHeader, StringComparison.OrdinalIgnoreCase)))
{
return tenantResult ?? Results.Forbid();
}
var content = await service.GetContentAsync(packId, cancellationToken).ConfigureAwait(false);
if (content is null)
{
return Results.NotFound();
}
context.Response.Headers["X-Content-Digest"] = record.Digest;
return Results.File(content, "application/octet-stream", fileDownloadName: packId + ".bin");
})
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status404NotFound);
app.MapGet("/api/v1/packs/{packId}/provenance", async (string packId, PackService service, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) =>
{
if (!IsAuthorized(context, auth, out var unauthorizedResult))
{
return unauthorizedResult;
}
var tenantHeader = context.Request.Headers["X-StellaOps-Tenant"].ToString();
var record = await service.GetAsync(packId, cancellationToken).ConfigureAwait(false);
if (record is null)
{
return Results.NotFound();
}
if (!IsTenantAllowed(record.TenantId, auth, out var tenantResult) ||
(!string.IsNullOrWhiteSpace(tenantHeader) && !string.Equals(record.TenantId, tenantHeader, StringComparison.OrdinalIgnoreCase)))
{
return tenantResult ?? Results.Forbid();
}
var content = await service.GetProvenanceAsync(packId, cancellationToken).ConfigureAwait(false);
if (content is null)
{
return Results.NotFound();
}
if (!string.IsNullOrWhiteSpace(record.ProvenanceDigest))
{
context.Response.Headers["X-Provenance-Digest"] = record.ProvenanceDigest;
}
return Results.File(content, "application/json", fileDownloadName: packId + "-provenance.json");
})
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status404NotFound);
app.MapGet("/api/v1/packs/{packId}/manifest", async (string packId, PackService service, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) =>
{
if (!IsAuthorized(context, auth, out var unauthorizedResult))
{
return unauthorizedResult;
}
var tenantHeader = context.Request.Headers["X-StellaOps-Tenant"].ToString();
var record = await service.GetAsync(packId, cancellationToken).ConfigureAwait(false);
if (record is null)
{
return Results.NotFound();
}
if (!IsTenantAllowed(record.TenantId, auth, out var tenantResult) ||
(!string.IsNullOrWhiteSpace(tenantHeader) && !string.Equals(record.TenantId, tenantHeader, StringComparison.OrdinalIgnoreCase)))
{
return tenantResult ?? Results.Forbid();
}
var content = await service.GetContentAsync(packId, cancellationToken).ConfigureAwait(false);
var provenance = await service.GetProvenanceAsync(packId, cancellationToken).ConfigureAwait(false);
var manifest = new PackManifestResponse(
record.PackId,
record.TenantId,
record.Digest,
content?.LongLength ?? 0,
record.ProvenanceDigest,
provenance?.LongLength,
record.CreatedAtUtc,
record.Metadata);
return Results.Ok(manifest);
})
.Produces<PackManifestResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status404NotFound);
app.MapPost("/api/v1/packs/{packId}/signature", async (string packId, RotateSignatureRequest request, PackService service, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) =>
{
if (!IsAuthorized(context, auth, out var unauthorizedResult))
{
return unauthorizedResult;
}
var tenantHeader = context.Request.Headers["X-StellaOps-Tenant"].ToString();
if (string.IsNullOrWhiteSpace(tenantHeader))
{
return Results.BadRequest(new { error = "tenant_missing", message = "X-StellaOps-Tenant header is required." });
}
if (!IsTenantAllowed(tenantHeader, auth, out var tenantResult))
{
return tenantResult;
}
if (string.IsNullOrWhiteSpace(request.Signature))
{
return Results.BadRequest(new { error = "signature_missing", message = "signature is required." });
}
IPackSignatureVerifier? overrideVerifier = null;
if (!string.IsNullOrWhiteSpace(request.PublicKeyPem))
{
overrideVerifier = new RsaSignatureVerifier(request.PublicKeyPem!);
}
try
{
var updated = await service.RotateSignatureAsync(packId, tenantHeader, request.Signature!, overrideVerifier, cancellationToken).ConfigureAwait(false);
return Results.Ok(PackResponse.From(updated));
}
catch (Exception ex)
{
return Results.BadRequest(new { error = "signature_rotation_failed", message = ex.Message });
}
})
.Produces<PackResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status404NotFound);
app.MapPost("/api/v1/packs/{packId}/attestations", async (string packId, AttestationUploadRequest request, AttestationService attestationService, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) =>
{
if (!IsAuthorized(context, auth, out var unauthorizedResult))
{
return unauthorizedResult;
}
var tenantHeader = context.Request.Headers["X-StellaOps-Tenant"].ToString();
if (string.IsNullOrWhiteSpace(tenantHeader))
{
return Results.BadRequest(new { error = "tenant_missing", message = "X-StellaOps-Tenant header is required." });
}
if (!IsTenantAllowed(tenantHeader, auth, out var tenantResult))
{
return tenantResult;
}
if (string.IsNullOrWhiteSpace(request.Type) || string.IsNullOrWhiteSpace(request.Content))
{
return Results.BadRequest(new { error = "attestation_missing", message = "type and content are required." });
}
try
{
var bytes = Convert.FromBase64String(request.Content);
var record = await attestationService.UploadAsync(packId, tenantHeader, request.Type!, bytes, request.Notes, cancellationToken).ConfigureAwait(false);
return Results.Created($"/api/v1/packs/{packId}/attestations/{record.Type}", AttestationResponse.From(record));
}
catch (Exception ex)
{
return Results.BadRequest(new { error = "attestation_failed", message = ex.Message });
}
})
.Produces<AttestationResponse>(StatusCodes.Status201Created)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status404NotFound);
app.MapGet("/api/v1/packs/{packId}/attestations", async (string packId, AttestationService attestationService, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) =>
{
if (!IsAuthorized(context, auth, out var unauthorizedResult))
{
return unauthorizedResult;
}
var tenantHeader = context.Request.Headers["X-StellaOps-Tenant"].ToString();
var records = await attestationService.ListAsync(packId, cancellationToken).ConfigureAwait(false);
if (records.Count == 0)
{
return Results.NotFound();
}
if (!string.IsNullOrWhiteSpace(tenantHeader) && !records.All(r => string.Equals(r.TenantId, tenantHeader, StringComparison.OrdinalIgnoreCase)))
{
return Results.Forbid();
}
return Results.Ok(records.Select(AttestationResponse.From));
})
.Produces<IEnumerable<AttestationResponse>>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status404NotFound);
app.MapGet("/api/v1/packs/{packId}/attestations/{type}", async (string packId, string type, AttestationService attestationService, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) =>
{
if (!IsAuthorized(context, auth, out var unauthorizedResult))
{
return unauthorizedResult;
}
var tenantHeader = context.Request.Headers["X-StellaOps-Tenant"].ToString();
var record = await attestationService.GetAsync(packId, type, cancellationToken).ConfigureAwait(false);
if (record is null)
{
return Results.NotFound();
}
if (!string.IsNullOrWhiteSpace(tenantHeader) && !string.Equals(record.TenantId, tenantHeader, StringComparison.OrdinalIgnoreCase))
{
return Results.Forbid();
}
var content = await attestationService.GetContentAsync(packId, type, cancellationToken).ConfigureAwait(false);
if (content is null)
{
return Results.NotFound();
}
context.Response.Headers["X-Attestation-Digest"] = record.Digest;
return Results.File(content, "application/octet-stream", fileDownloadName: $"{packId}-{type}-attestation.bin");
})
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status404NotFound);
app.MapGet("/api/v1/packs/{packId}/parity", async (string packId, ParityService parityService, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) =>
{
if (!IsAuthorized(context, auth, out var unauthorizedResult))
{
return unauthorizedResult;
}
var tenantHeader = context.Request.Headers["X-StellaOps-Tenant"].ToString();
var parity = await parityService.GetAsync(packId, cancellationToken).ConfigureAwait(false);
if (parity is null)
{
return Results.NotFound();
}
if (!IsTenantAllowed(parity.TenantId, auth, out var tenantResult) ||
(!string.IsNullOrWhiteSpace(tenantHeader) && !string.Equals(parity.TenantId, tenantHeader, StringComparison.OrdinalIgnoreCase)))
{
return tenantResult ?? Results.Forbid();
}
return Results.Ok(ParityResponse.From(parity));
})
.Produces<ParityResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status404NotFound);
app.MapGet("/api/v1/packs/{packId}/lifecycle", async (string packId, LifecycleService lifecycleService, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) =>
{
if (!IsAuthorized(context, auth, out var unauthorizedResult))
{
return unauthorizedResult;
}
var tenantHeader = context.Request.Headers["X-StellaOps-Tenant"].ToString();
var record = await lifecycleService.GetAsync(packId, cancellationToken).ConfigureAwait(false);
if (record is null)
{
return Results.NotFound();
}
if (!IsTenantAllowed(record.TenantId, auth, out var tenantResult) ||
(!string.IsNullOrWhiteSpace(tenantHeader) && !string.Equals(record.TenantId, tenantHeader, StringComparison.OrdinalIgnoreCase)))
{
return tenantResult ?? Results.Forbid();
}
return Results.Ok(LifecycleResponse.From(record));
})
.Produces<LifecycleResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status404NotFound);
app.MapPost("/api/v1/packs/{packId}/lifecycle", async (string packId, LifecycleRequest request, LifecycleService lifecycleService, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) =>
{
if (!IsAuthorized(context, auth, out var unauthorizedResult))
{
return unauthorizedResult;
}
var tenantHeader = context.Request.Headers["X-StellaOps-Tenant"].ToString();
var tenant = !string.IsNullOrWhiteSpace(tenantHeader) ? tenantHeader : null;
if (string.IsNullOrWhiteSpace(tenant))
{
return Results.BadRequest(new { error = "tenant_missing", message = "X-StellaOps-Tenant header is required." });
}
if (!IsTenantAllowed(tenant, auth, out var tenantResult))
{
return tenantResult;
}
if (string.IsNullOrWhiteSpace(request.State))
{
return Results.BadRequest(new { error = "state_missing", message = "state is required." });
}
try
{
var record = await lifecycleService.SetStateAsync(packId, tenant, request.State!, request.Notes, cancellationToken).ConfigureAwait(false);
return Results.Ok(LifecycleResponse.From(record));
}
catch (Exception ex)
{
return Results.BadRequest(new { error = "lifecycle_failed", message = ex.Message });
}
})
.Produces<LifecycleResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status404NotFound);
app.MapPost("/api/v1/packs/{packId}/parity", async (string packId, ParityRequest request, ParityService parityService, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) =>
{
if (!IsAuthorized(context, auth, out var unauthorizedResult))
{
return unauthorizedResult;
}
var tenantHeader = context.Request.Headers["X-StellaOps-Tenant"].ToString();
var tenant = !string.IsNullOrWhiteSpace(tenantHeader) ? tenantHeader : null;
if (string.IsNullOrWhiteSpace(tenant))
{
return Results.BadRequest(new { error = "tenant_missing", message = "X-StellaOps-Tenant header is required." });
}
if (!IsTenantAllowed(tenant, auth, out var tenantResult))
{
return tenantResult;
}
if (string.IsNullOrWhiteSpace(request.Status))
{
return Results.BadRequest(new { error = "status_missing", message = "status is required." });
}
try
{
var record = await parityService.SetStatusAsync(packId, tenant, request.Status!, request.Notes, cancellationToken).ConfigureAwait(false);
return Results.Ok(ParityResponse.From(record));
}
catch (Exception ex)
{
return Results.BadRequest(new { error = "parity_failed", message = ex.Message });
}
})
.Produces<ParityResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status404NotFound);
app.MapPost("/api/v1/export/offline-seed", async (OfflineSeedRequest request, ExportService exportService, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) =>
{
if (!IsAuthorized(context, auth, out var unauthorizedResult))
{
return unauthorizedResult;
}
var tenantHeader = context.Request.Headers["X-StellaOps-Tenant"].ToString();
var tenant = !string.IsNullOrWhiteSpace(request.TenantId) ? request.TenantId : tenantHeader;
if (auth.AllowedTenants is { Length: > 0 } && string.IsNullOrWhiteSpace(tenant))
{
return Results.BadRequest(new { error = "tenant_missing", message = "tenantId or X-StellaOps-Tenant header is required when tenant allowlists are configured." });
}
if (!string.IsNullOrWhiteSpace(tenant) && !IsTenantAllowed(tenant, auth, out var tenantResult))
{
return tenantResult;
}
var archive = await exportService.ExportOfflineSeedAsync(tenant, request.IncludeContent, request.IncludeProvenance, cancellationToken).ConfigureAwait(false);
return Results.File(archive, "application/zip", fileDownloadName: "packs-offline-seed.zip");
})
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden);
app.MapPost("/api/v1/mirrors", async (MirrorRequest request, MirrorService mirrorService, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) =>
{
if (!IsAuthorized(context, auth, out var unauthorizedResult))
{
return unauthorizedResult;
}
var tenantHeader = context.Request.Headers["X-StellaOps-Tenant"].ToString();
if (string.IsNullOrWhiteSpace(tenantHeader))
{
return Results.BadRequest(new { error = "tenant_missing", message = "X-StellaOps-Tenant header is required." });
}
if (!IsTenantAllowed(tenantHeader, auth, out var tenantResult))
{
return tenantResult;
}
if (string.IsNullOrWhiteSpace(request.Id) || string.IsNullOrWhiteSpace(request.Upstream))
{
return Results.BadRequest(new { error = "mirror_missing", message = "id and upstream are required." });
}
var record = await mirrorService.UpsertAsync(request.Id!, tenantHeader, new Uri(request.Upstream!), request.Enabled, request.Notes, cancellationToken).ConfigureAwait(false);
return Results.Created($"/api/v1/mirrors/{record.Id}", MirrorResponse.From(record));
})
.Produces<MirrorResponse>(StatusCodes.Status201Created)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden);
app.MapGet("/api/v1/mirrors", async (string? tenant, MirrorService mirrorService, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) =>
{
if (!IsAuthorized(context, auth, out var unauthorizedResult))
{
return unauthorizedResult;
}
var tenantHeader = context.Request.Headers["X-StellaOps-Tenant"].ToString();
var effectiveTenant = !string.IsNullOrWhiteSpace(tenant) ? tenant : tenantHeader;
if (!string.IsNullOrWhiteSpace(effectiveTenant) && !IsTenantAllowed(effectiveTenant, auth, out var tenantResult))
{
return tenantResult;
}
var mirrors = await mirrorService.ListAsync(effectiveTenant, cancellationToken).ConfigureAwait(false);
return Results.Ok(mirrors.Select(MirrorResponse.From));
})
.Produces<IEnumerable<MirrorResponse>>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden);
app.MapPost("/api/v1/mirrors/{id}/sync", async (string id, MirrorSyncRequest request, MirrorService mirrorService, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) =>
{
if (!IsAuthorized(context, auth, out var unauthorizedResult))
{
return unauthorizedResult;
}
var tenantHeader = context.Request.Headers["X-StellaOps-Tenant"].ToString();
if (string.IsNullOrWhiteSpace(tenantHeader))
{
return Results.BadRequest(new { error = "tenant_missing", message = "X-StellaOps-Tenant header is required." });
}
var updated = await mirrorService.MarkSyncAsync(id, tenantHeader, request.Status ?? "unknown", request.Notes, cancellationToken).ConfigureAwait(false);
if (updated is null)
{
return Results.NotFound();
}
return Results.Ok(MirrorResponse.From(updated));
})
.Produces<MirrorResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status404NotFound);
app.MapGet("/api/v1/compliance/summary", async (string? tenant, ComplianceService complianceService, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) =>
{
if (!IsAuthorized(context, auth, out var unauthorizedResult))
{
return unauthorizedResult;
}
var tenantHeader = context.Request.Headers["X-StellaOps-Tenant"].ToString();
var effectiveTenant = !string.IsNullOrWhiteSpace(tenant) ? tenant : tenantHeader;
if (!string.IsNullOrWhiteSpace(effectiveTenant) && !IsTenantAllowed(effectiveTenant, auth, out var tenantResult))
{
return tenantResult;
}
var summary = await complianceService.SummarizeAsync(effectiveTenant, cancellationToken).ConfigureAwait(false);
return Results.Ok(summary);
})
.Produces<ComplianceSummary>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden);
app.Run();
static bool IsAuthorized(HttpContext context, AuthOptions auth, out IResult result)
{
result = Results.Empty;
if (string.IsNullOrWhiteSpace(auth.ApiKey))
{
return true; // auth disabled
}
var provided = context.Request.Headers["X-API-Key"].ToString();
if (string.Equals(provided, auth.ApiKey, StringComparison.Ordinal))
{
return true;
}
result = Results.Unauthorized();
return false;
}
static bool IsTenantAllowed(string tenant, AuthOptions auth, out IResult? result)
{
result = null;
if (auth.AllowedTenants is { Length: > 0 } && !auth.AllowedTenants.Any(t => string.Equals(t, tenant, StringComparison.OrdinalIgnoreCase)))
{
result = Results.Forbid();
return false;
}
return true;
}
public partial class Program;