using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using StellaOps.Auth.Abstractions; using StellaOps.Policy.Engine.Attestation; namespace StellaOps.Policy.Engine.Endpoints; /// /// Endpoints for verification policy management per CONTRACT-VERIFICATION-POLICY-006. /// public static class VerificationPolicyEndpoints { public static IEndpointRouteBuilder MapVerificationPolicies(this IEndpointRouteBuilder routes) { var group = routes.MapGroup("/api/v1/attestor/policies") .WithTags("Verification Policies"); group.MapPost("/", CreatePolicyAsync) .WithName("Attestor.CreatePolicy") .WithSummary("Create a new verification policy") .RequireAuthorization(policy => policy.RequireClaim("scope", StellaOpsScopes.PolicyWrite)) .Produces(StatusCodes.Status201Created) .Produces(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status409Conflict); group.MapGet("/{policyId}", GetPolicyAsync) .WithName("Attestor.GetPolicy") .WithSummary("Get a verification policy by ID") .RequireAuthorization(policy => policy.RequireClaim("scope", StellaOpsScopes.PolicyRead)) .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound); group.MapGet("/", ListPoliciesAsync) .WithName("Attestor.ListPolicies") .WithSummary("List verification policies") .RequireAuthorization(policy => policy.RequireClaim("scope", StellaOpsScopes.PolicyRead)) .Produces(StatusCodes.Status200OK); group.MapPut("/{policyId}", UpdatePolicyAsync) .WithName("Attestor.UpdatePolicy") .WithSummary("Update a verification policy") .RequireAuthorization(policy => policy.RequireClaim("scope", StellaOpsScopes.PolicyWrite)) .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound); group.MapDelete("/{policyId}", DeletePolicyAsync) .WithName("Attestor.DeletePolicy") .WithSummary("Delete a verification policy") .RequireAuthorization(policy => policy.RequireClaim("scope", StellaOpsScopes.PolicyWrite)) .Produces(StatusCodes.Status204NoContent) .Produces(StatusCodes.Status404NotFound); return routes; } private static async Task CreatePolicyAsync( [FromBody] CreateVerificationPolicyRequest request, IVerificationPolicyStore store, TimeProvider timeProvider, CancellationToken cancellationToken) { if (request == null) { return Results.BadRequest(CreateProblem( "Invalid request", "Request body is required.", "ERR_ATTEST_001")); } if (string.IsNullOrWhiteSpace(request.PolicyId)) { return Results.BadRequest(CreateProblem( "Invalid request", "Policy ID is required.", "ERR_ATTEST_002")); } if (request.PredicateTypes == null || request.PredicateTypes.Count == 0) { return Results.BadRequest(CreateProblem( "Invalid request", "At least one predicate type is required.", "ERR_ATTEST_003")); } if (await store.ExistsAsync(request.PolicyId, cancellationToken).ConfigureAwait(false)) { return Results.Conflict(CreateProblem( "Policy exists", $"Policy '{request.PolicyId}' already exists.", "ERR_ATTEST_004")); } var now = timeProvider.GetUtcNow(); var policy = new VerificationPolicy( PolicyId: request.PolicyId, Version: request.Version ?? "1.0.0", Description: request.Description, TenantScope: request.TenantScope ?? "*", PredicateTypes: request.PredicateTypes, SignerRequirements: request.SignerRequirements ?? SignerRequirements.Default, ValidityWindow: request.ValidityWindow, Metadata: request.Metadata, CreatedAt: now, UpdatedAt: now); await store.CreateAsync(policy, cancellationToken).ConfigureAwait(false); return Results.Created( $"/api/v1/attestor/policies/{policy.PolicyId}", policy); } private static async Task GetPolicyAsync( [FromRoute] string policyId, IVerificationPolicyStore store, CancellationToken cancellationToken) { var policy = await store.GetAsync(policyId, cancellationToken).ConfigureAwait(false); if (policy == null) { return Results.NotFound(CreateProblem( "Policy not found", $"Policy '{policyId}' was not found.", "ERR_ATTEST_005")); } return Results.Ok(policy); } private static async Task ListPoliciesAsync( [FromQuery] string? tenantScope, IVerificationPolicyStore store, CancellationToken cancellationToken) { var policies = await store.ListAsync(tenantScope, cancellationToken).ConfigureAwait(false); return Results.Ok(new VerificationPolicyListResponse( Policies: policies, Total: policies.Count)); } private static async Task UpdatePolicyAsync( [FromRoute] string policyId, [FromBody] UpdateVerificationPolicyRequest request, IVerificationPolicyStore store, TimeProvider timeProvider, CancellationToken cancellationToken) { if (request == null) { return Results.BadRequest(CreateProblem( "Invalid request", "Request body is required.", "ERR_ATTEST_001")); } var now = timeProvider.GetUtcNow(); var updated = await store.UpdateAsync( policyId, existing => existing with { Version = request.Version ?? existing.Version, Description = request.Description ?? existing.Description, PredicateTypes = request.PredicateTypes ?? existing.PredicateTypes, SignerRequirements = request.SignerRequirements ?? existing.SignerRequirements, ValidityWindow = request.ValidityWindow ?? existing.ValidityWindow, Metadata = request.Metadata ?? existing.Metadata, UpdatedAt = now }, cancellationToken).ConfigureAwait(false); if (updated == null) { return Results.NotFound(CreateProblem( "Policy not found", $"Policy '{policyId}' was not found.", "ERR_ATTEST_005")); } return Results.Ok(updated); } private static async Task DeletePolicyAsync( [FromRoute] string policyId, IVerificationPolicyStore store, CancellationToken cancellationToken) { var deleted = await store.DeleteAsync(policyId, cancellationToken).ConfigureAwait(false); if (!deleted) { return Results.NotFound(CreateProblem( "Policy not found", $"Policy '{policyId}' was not found.", "ERR_ATTEST_005")); } return Results.NoContent(); } private static ProblemDetails CreateProblem(string title, string detail, string? errorCode = null) { var problem = new ProblemDetails { Title = title, Detail = detail, Status = StatusCodes.Status400BadRequest }; if (!string.IsNullOrWhiteSpace(errorCode)) { problem.Extensions["error_code"] = errorCode; } return problem; } } /// /// Response for listing verification policies. /// public sealed record VerificationPolicyListResponse( [property: System.Text.Json.Serialization.JsonPropertyName("policies")] IReadOnlyList Policies, [property: System.Text.Json.Serialization.JsonPropertyName("total")] int Total);