using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Platform.WebService.Constants; using StellaOps.Platform.WebService.Contracts; using StellaOps.Platform.WebService.Services; namespace StellaOps.Platform.WebService.Endpoints; /// /// Trust and signing owner mutation endpoints backing Administration A6. /// public static class AdministrationTrustSigningMutationEndpoints { private const int DefaultLimit = 50; private const int MaxLimit = 200; public static IEndpointRouteBuilder MapAdministrationTrustSigningMutationEndpoints(this IEndpointRouteBuilder app) { var group = app.MapGroup("/api/v1/administration/trust-signing") .WithTags("Administration") .RequireAuthorization(PlatformPolicies.TrustRead) .RequireTenant(); group.MapGet("/keys", async Task( HttpContext context, PlatformRequestContextResolver resolver, IAdministrationTrustSigningStore store, TimeProvider timeProvider, [FromQuery] int? limit, [FromQuery] int? offset, CancellationToken cancellationToken) => { if (!TryResolveContext(context, resolver, out var requestContext, out var failure)) { return failure!; } var normalizedLimit = NormalizeLimit(limit); var normalizedOffset = NormalizeOffset(offset); var items = await store.ListKeysAsync( requestContext!.TenantId, normalizedLimit, normalizedOffset, cancellationToken).ConfigureAwait(false); return Results.Ok(new PlatformListResponse( requestContext.TenantId, requestContext.ActorId, timeProvider.GetUtcNow(), Cached: false, CacheTtlSeconds: 0, items, items.Count, normalizedLimit, normalizedOffset)); }) .WithName("ListAdministrationTrustKeys") .WithSummary("List trust signing keys") .RequireAuthorization(PlatformPolicies.TrustRead); group.MapPost("/keys", async Task( HttpContext context, PlatformRequestContextResolver resolver, IAdministrationTrustSigningStore store, CreateAdministrationTrustKeyRequest request, CancellationToken cancellationToken) => { if (!TryResolveContext(context, resolver, out var requestContext, out var failure)) { return failure!; } try { var created = await store.CreateKeyAsync( requestContext!.TenantId, requestContext.ActorId, request, cancellationToken).ConfigureAwait(false); return Results.Created($"/api/v1/administration/trust-signing/keys/{created.KeyId}", created); } catch (InvalidOperationException ex) { return MapStoreError(ex, keyId: null, certificateId: null); } }) .WithName("CreateAdministrationTrustKey") .WithSummary("Create trust signing key") .RequireAuthorization(PlatformPolicies.TrustWrite); group.MapPost("/keys/{keyId:guid}/rotate", async Task( HttpContext context, PlatformRequestContextResolver resolver, IAdministrationTrustSigningStore store, Guid keyId, RotateAdministrationTrustKeyRequest request, CancellationToken cancellationToken) => { if (!TryResolveContext(context, resolver, out var requestContext, out var failure)) { return failure!; } try { var updated = await store.RotateKeyAsync( requestContext!.TenantId, requestContext.ActorId, keyId, request, cancellationToken).ConfigureAwait(false); return Results.Ok(updated); } catch (InvalidOperationException ex) { return MapStoreError(ex, keyId, certificateId: null); } }) .WithName("RotateAdministrationTrustKey") .WithSummary("Rotate trust signing key") .RequireAuthorization(PlatformPolicies.TrustWrite); group.MapPost("/keys/{keyId:guid}/revoke", async Task( HttpContext context, PlatformRequestContextResolver resolver, IAdministrationTrustSigningStore store, Guid keyId, RevokeAdministrationTrustKeyRequest request, CancellationToken cancellationToken) => { if (!TryResolveContext(context, resolver, out var requestContext, out var failure)) { return failure!; } try { var updated = await store.RevokeKeyAsync( requestContext!.TenantId, requestContext.ActorId, keyId, request, cancellationToken).ConfigureAwait(false); return Results.Ok(updated); } catch (InvalidOperationException ex) { return MapStoreError(ex, keyId, certificateId: null); } }) .WithName("RevokeAdministrationTrustKey") .WithSummary("Revoke trust signing key") .RequireAuthorization(PlatformPolicies.TrustAdmin); group.MapGet("/issuers", async Task( HttpContext context, PlatformRequestContextResolver resolver, IAdministrationTrustSigningStore store, TimeProvider timeProvider, [FromQuery] int? limit, [FromQuery] int? offset, CancellationToken cancellationToken) => { if (!TryResolveContext(context, resolver, out var requestContext, out var failure)) { return failure!; } var normalizedLimit = NormalizeLimit(limit); var normalizedOffset = NormalizeOffset(offset); var items = await store.ListIssuersAsync( requestContext!.TenantId, normalizedLimit, normalizedOffset, cancellationToken).ConfigureAwait(false); return Results.Ok(new PlatformListResponse( requestContext.TenantId, requestContext.ActorId, timeProvider.GetUtcNow(), Cached: false, CacheTtlSeconds: 0, items, items.Count, normalizedLimit, normalizedOffset)); }) .WithName("ListAdministrationTrustIssuers") .WithSummary("List trust issuers") .RequireAuthorization(PlatformPolicies.TrustRead); group.MapPost("/issuers", async Task( HttpContext context, PlatformRequestContextResolver resolver, IAdministrationTrustSigningStore store, RegisterAdministrationTrustIssuerRequest request, CancellationToken cancellationToken) => { if (!TryResolveContext(context, resolver, out var requestContext, out var failure)) { return failure!; } try { var created = await store.RegisterIssuerAsync( requestContext!.TenantId, requestContext.ActorId, request, cancellationToken).ConfigureAwait(false); return Results.Created($"/api/v1/administration/trust-signing/issuers/{created.IssuerId}", created); } catch (InvalidOperationException ex) { return MapStoreError(ex, keyId: null, certificateId: null); } }) .WithName("RegisterAdministrationTrustIssuer") .WithSummary("Register trust issuer") .RequireAuthorization(PlatformPolicies.TrustWrite); group.MapPost("/issuers/{issuerId:guid}/block", async Task( HttpContext context, PlatformRequestContextResolver resolver, IAdministrationTrustSigningStore store, Guid issuerId, BlockAdministrationTrustIssuerRequest request, CancellationToken cancellationToken) => { if (!TryResolveContext(context, resolver, out var requestContext, out var failure)) { return failure!; } try { var updated = await store.BlockIssuerAsync( requestContext!.TenantId, requestContext.ActorId, issuerId, request, cancellationToken).ConfigureAwait(false); return Results.Ok(updated); } catch (InvalidOperationException ex) { return MapStoreError(ex, keyId: null, certificateId: null); } }) .WithName("BlockAdministrationTrustIssuer") .WithSummary("Block trust issuer") .RequireAuthorization(PlatformPolicies.TrustAdmin); group.MapPost("/issuers/{issuerId:guid}/unblock", async Task( HttpContext context, PlatformRequestContextResolver resolver, IAdministrationTrustSigningStore store, Guid issuerId, UnblockAdministrationTrustIssuerRequest request, CancellationToken cancellationToken) => { if (!TryResolveContext(context, resolver, out var requestContext, out var failure)) { return failure!; } try { var updated = await store.UnblockIssuerAsync( requestContext!.TenantId, requestContext.ActorId, issuerId, request, cancellationToken).ConfigureAwait(false); return Results.Ok(updated); } catch (InvalidOperationException ex) { return MapStoreError(ex, keyId: null, certificateId: null); } }) .WithName("UnblockAdministrationTrustIssuer") .WithSummary("Unblock trust issuer") .RequireAuthorization(PlatformPolicies.TrustAdmin); group.MapGet("/certificates", async Task( HttpContext context, PlatformRequestContextResolver resolver, IAdministrationTrustSigningStore store, TimeProvider timeProvider, [FromQuery] int? limit, [FromQuery] int? offset, CancellationToken cancellationToken) => { if (!TryResolveContext(context, resolver, out var requestContext, out var failure)) { return failure!; } var normalizedLimit = NormalizeLimit(limit); var normalizedOffset = NormalizeOffset(offset); var items = await store.ListCertificatesAsync( requestContext!.TenantId, normalizedLimit, normalizedOffset, cancellationToken).ConfigureAwait(false); return Results.Ok(new PlatformListResponse( requestContext.TenantId, requestContext.ActorId, timeProvider.GetUtcNow(), Cached: false, CacheTtlSeconds: 0, items, items.Count, normalizedLimit, normalizedOffset)); }) .WithName("ListAdministrationTrustCertificates") .WithSummary("List trust certificates") .RequireAuthorization(PlatformPolicies.TrustRead); group.MapPost("/certificates", async Task( HttpContext context, PlatformRequestContextResolver resolver, IAdministrationTrustSigningStore store, RegisterAdministrationTrustCertificateRequest request, CancellationToken cancellationToken) => { if (!TryResolveContext(context, resolver, out var requestContext, out var failure)) { return failure!; } try { var created = await store.RegisterCertificateAsync( requestContext!.TenantId, requestContext.ActorId, request, cancellationToken).ConfigureAwait(false); return Results.Created($"/api/v1/administration/trust-signing/certificates/{created.CertificateId}", created); } catch (InvalidOperationException ex) { return MapStoreError(ex, keyId: null, certificateId: null); } }) .WithName("RegisterAdministrationTrustCertificate") .WithSummary("Register trust certificate") .RequireAuthorization(PlatformPolicies.TrustWrite); group.MapPost("/certificates/{certificateId:guid}/revoke", async Task( HttpContext context, PlatformRequestContextResolver resolver, IAdministrationTrustSigningStore store, Guid certificateId, RevokeAdministrationTrustCertificateRequest request, CancellationToken cancellationToken) => { if (!TryResolveContext(context, resolver, out var requestContext, out var failure)) { return failure!; } try { var updated = await store.RevokeCertificateAsync( requestContext!.TenantId, requestContext.ActorId, certificateId, request, cancellationToken).ConfigureAwait(false); return Results.Ok(updated); } catch (InvalidOperationException ex) { return MapStoreError(ex, keyId: null, certificateId); } }) .WithName("RevokeAdministrationTrustCertificate") .WithSummary("Revoke trust certificate") .RequireAuthorization(PlatformPolicies.TrustAdmin); group.MapGet("/transparency-log", async Task( HttpContext context, PlatformRequestContextResolver resolver, IAdministrationTrustSigningStore store, TimeProvider timeProvider, CancellationToken cancellationToken) => { if (!TryResolveContext(context, resolver, out var requestContext, out var failure)) { return failure!; } var config = await store.GetTransparencyLogConfigAsync( requestContext!.TenantId, cancellationToken).ConfigureAwait(false); if (config is null) { return Results.NotFound(new { error = "transparency_log_not_configured" }); } return Results.Ok(new PlatformItemResponse( requestContext.TenantId, requestContext.ActorId, timeProvider.GetUtcNow(), Cached: false, CacheTtlSeconds: 0, config)); }) .WithName("GetAdministrationTrustTransparencyLog") .WithSummary("Get trust transparency log configuration") .RequireAuthorization(PlatformPolicies.TrustRead); group.MapPut("/transparency-log", async Task( HttpContext context, PlatformRequestContextResolver resolver, IAdministrationTrustSigningStore store, ConfigureAdministrationTransparencyLogRequest request, CancellationToken cancellationToken) => { if (!TryResolveContext(context, resolver, out var requestContext, out var failure)) { return failure!; } try { var config = await store.ConfigureTransparencyLogAsync( requestContext!.TenantId, requestContext.ActorId, request, cancellationToken).ConfigureAwait(false); return Results.Ok(config); } catch (InvalidOperationException ex) { return MapStoreError(ex, keyId: null, certificateId: null); } }) .WithName("ConfigureAdministrationTrustTransparencyLog") .WithSummary("Configure trust transparency log") .RequireAuthorization(PlatformPolicies.TrustAdmin); return app; } private static IResult MapStoreError(InvalidOperationException exception, Guid? keyId, Guid? certificateId) { return exception.Message switch { "request_required" => Results.BadRequest(new { error = "request_required" }), "tenant_required" => Results.BadRequest(new { error = "tenant_required" }), "tenant_id_invalid" => Results.BadRequest(new { error = "tenant_id_invalid" }), "reason_required" => Results.BadRequest(new { error = "reason_required" }), "key_alias_required" => Results.BadRequest(new { error = "key_alias_required" }), "key_algorithm_required" => Results.BadRequest(new { error = "key_algorithm_required" }), "key_alias_exists" => Results.Conflict(new { error = "key_alias_exists" }), "key_not_found" => Results.NotFound(new { error = "key_not_found", keyId }), "key_revoked" => Results.Conflict(new { error = "key_revoked", keyId }), "issuer_name_required" => Results.BadRequest(new { error = "issuer_name_required" }), "issuer_uri_required" => Results.BadRequest(new { error = "issuer_uri_required" }), "issuer_uri_invalid" => Results.BadRequest(new { error = "issuer_uri_invalid" }), "issuer_trust_level_required" => Results.BadRequest(new { error = "issuer_trust_level_required" }), "issuer_uri_exists" => Results.Conflict(new { error = "issuer_uri_exists" }), "issuer_not_found" => Results.NotFound(new { error = "issuer_not_found" }), "certificate_serial_required" => Results.BadRequest(new { error = "certificate_serial_required" }), "certificate_validity_invalid" => Results.BadRequest(new { error = "certificate_validity_invalid" }), "certificate_serial_exists" => Results.Conflict(new { error = "certificate_serial_exists" }), "certificate_not_found" => Results.NotFound(new { error = "certificate_not_found", certificateId }), "transparency_log_url_required" => Results.BadRequest(new { error = "transparency_log_url_required" }), "transparency_log_url_invalid" => Results.BadRequest(new { error = "transparency_log_url_invalid" }), "transparency_witness_url_invalid" => Results.BadRequest(new { error = "transparency_witness_url_invalid" }), _ => Results.BadRequest(new { error = exception.Message }) }; } private static int NormalizeLimit(int? value) { return value switch { null => DefaultLimit, < 1 => 1, > MaxLimit => MaxLimit, _ => value.Value }; } private static int NormalizeOffset(int? value) => value is null or < 0 ? 0 : value.Value; private static bool TryResolveContext( HttpContext context, PlatformRequestContextResolver resolver, out PlatformRequestContext? requestContext, out IResult? failure) { if (resolver.TryResolve(context, out requestContext, out var error)) { failure = null; return true; } failure = Results.BadRequest(new { error = error ?? "tenant_missing" }); return false; } }