- Added IIssuerDirectory interface for managing VEX document issuers, including methods for registration, revocation, and trust validation. - Created InMemoryIssuerDirectory class as an in-memory implementation of IIssuerDirectory for testing and single-instance deployments. - Introduced ISignatureVerifier interface for verifying signatures on VEX documents, with support for multiple signature formats. - Developed SignatureVerifier class as the default implementation of ISignatureVerifier, allowing extensibility for different signature formats. - Implemented handlers for DSSE and JWS signature formats, including methods for verification and signature extraction. - Defined various records and enums for issuer and signature metadata, enhancing the structure and clarity of the verification process.
228 lines
8.3 KiB
C#
228 lines
8.3 KiB
C#
using Microsoft.AspNetCore.Http.HttpResults;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using StellaOps.Auth.Abstractions;
|
|
using StellaOps.Policy.Engine.Attestation;
|
|
|
|
namespace StellaOps.Policy.Engine.Endpoints;
|
|
|
|
/// <summary>
|
|
/// Endpoints for verification policy management per CONTRACT-VERIFICATION-POLICY-006.
|
|
/// </summary>
|
|
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<VerificationPolicy>(StatusCodes.Status201Created)
|
|
.Produces<ProblemHttpResult>(StatusCodes.Status400BadRequest)
|
|
.Produces<ProblemHttpResult>(StatusCodes.Status409Conflict);
|
|
|
|
group.MapGet("/{policyId}", GetPolicyAsync)
|
|
.WithName("Attestor.GetPolicy")
|
|
.WithSummary("Get a verification policy by ID")
|
|
.RequireAuthorization(policy => policy.RequireClaim("scope", StellaOpsScopes.PolicyRead))
|
|
.Produces<VerificationPolicy>(StatusCodes.Status200OK)
|
|
.Produces<ProblemHttpResult>(StatusCodes.Status404NotFound);
|
|
|
|
group.MapGet("/", ListPoliciesAsync)
|
|
.WithName("Attestor.ListPolicies")
|
|
.WithSummary("List verification policies")
|
|
.RequireAuthorization(policy => policy.RequireClaim("scope", StellaOpsScopes.PolicyRead))
|
|
.Produces<VerificationPolicyListResponse>(StatusCodes.Status200OK);
|
|
|
|
group.MapPut("/{policyId}", UpdatePolicyAsync)
|
|
.WithName("Attestor.UpdatePolicy")
|
|
.WithSummary("Update a verification policy")
|
|
.RequireAuthorization(policy => policy.RequireClaim("scope", StellaOpsScopes.PolicyWrite))
|
|
.Produces<VerificationPolicy>(StatusCodes.Status200OK)
|
|
.Produces<ProblemHttpResult>(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<ProblemHttpResult>(StatusCodes.Status404NotFound);
|
|
|
|
return routes;
|
|
}
|
|
|
|
private static async Task<IResult> 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<IResult> 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<IResult> 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<IResult> 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<IResult> 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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Response for listing verification policies.
|
|
/// </summary>
|
|
public sealed record VerificationPolicyListResponse(
|
|
[property: System.Text.Json.Serialization.JsonPropertyName("policies")] IReadOnlyList<VerificationPolicy> Policies,
|
|
[property: System.Text.Json.Serialization.JsonPropertyName("total")] int Total);
|