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);