Restructure solution layout by module

This commit is contained in:
master
2025-10-28 15:10:40 +02:00
parent 95daa159c4
commit d870da18ce
4103 changed files with 192899 additions and 187024 deletions

View File

@@ -0,0 +1,267 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using StellaOps.Auth.Abstractions;
using StellaOps.Policy.Engine.Domain;
using StellaOps.Policy.Engine.Services;
namespace StellaOps.Policy.Engine.Endpoints;
internal static class PolicyPackEndpoints
{
public static IEndpointRouteBuilder MapPolicyPacks(this IEndpointRouteBuilder endpoints)
{
var group = endpoints.MapGroup("/api/policy/packs")
.RequireAuthorization()
.WithTags("Policy Packs");
group.MapPost(string.Empty, CreatePack)
.WithName("CreatePolicyPack")
.WithSummary("Create a new policy pack container.")
.Produces<PolicyPackDto>(StatusCodes.Status201Created);
group.MapGet(string.Empty, ListPacks)
.WithName("ListPolicyPacks")
.WithSummary("List policy packs for the current tenant.")
.Produces<IReadOnlyList<PolicyPackSummaryDto>>(StatusCodes.Status200OK);
group.MapPost("/{packId}/revisions", CreateRevision)
.WithName("CreatePolicyRevision")
.WithSummary("Create or update policy revision metadata.")
.Produces<PolicyRevisionDto>(StatusCodes.Status201Created)
.Produces<ProblemHttpResult>(StatusCodes.Status400BadRequest);
group.MapPost("/{packId}/revisions/{version:int}:activate", ActivateRevision)
.WithName("ActivatePolicyRevision")
.WithSummary("Activate an approved policy revision, enforcing two-person approval when required.")
.Produces<PolicyRevisionActivationResponse>(StatusCodes.Status200OK)
.Produces<PolicyRevisionActivationResponse>(StatusCodes.Status202Accepted)
.Produces<ProblemHttpResult>(StatusCodes.Status400BadRequest)
.Produces<ProblemHttpResult>(StatusCodes.Status404NotFound);
return endpoints;
}
private static async Task<IResult> CreatePack(
HttpContext context,
[FromBody] CreatePolicyPackRequest request,
IPolicyPackRepository repository,
CancellationToken cancellationToken)
{
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyEdit);
if (scopeResult is not null)
{
return scopeResult;
}
if (request is null)
{
return Results.BadRequest(new ProblemDetails
{
Title = "Invalid request",
Detail = "Request body is required.",
Status = StatusCodes.Status400BadRequest
});
}
var packId = string.IsNullOrWhiteSpace(request.PackId)
? $"pack-{Guid.NewGuid():n}"
: request.PackId.Trim();
var pack = await repository.CreateAsync(packId, request.DisplayName?.Trim(), cancellationToken).ConfigureAwait(false);
var dto = PolicyPackMapper.ToDto(pack);
return Results.Created($"/api/policy/packs/{dto.PackId}", dto);
}
private static async Task<IResult> ListPacks(
HttpContext context,
IPolicyPackRepository repository,
CancellationToken cancellationToken)
{
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead);
if (scopeResult is not null)
{
return scopeResult;
}
var packs = await repository.ListAsync(cancellationToken).ConfigureAwait(false);
var summaries = packs.Select(PolicyPackMapper.ToSummaryDto).ToArray();
return Results.Ok(summaries);
}
private static async Task<IResult> CreateRevision(
HttpContext context,
[FromRoute] string packId,
[FromBody] CreatePolicyRevisionRequest request,
IPolicyPackRepository repository,
CancellationToken cancellationToken)
{
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyEdit);
if (scopeResult is not null)
{
return scopeResult;
}
if (request is null)
{
return Results.BadRequest(new ProblemDetails
{
Title = "Invalid request",
Detail = "Request body is required.",
Status = StatusCodes.Status400BadRequest
});
}
if (request.InitialStatus is not (PolicyRevisionStatus.Draft or PolicyRevisionStatus.Approved))
{
return Results.BadRequest(new ProblemDetails
{
Title = "Invalid status",
Detail = "Only Draft or Approved statuses are supported for new revisions.",
Status = StatusCodes.Status400BadRequest
});
}
var revision = await repository.UpsertRevisionAsync(
packId,
request.Version ?? 0,
request.RequiresTwoPersonApproval,
request.InitialStatus,
cancellationToken).ConfigureAwait(false);
return Results.Created(
$"/api/policy/packs/{packId}/revisions/{revision.Version}",
PolicyPackMapper.ToDto(packId, revision));
}
private static async Task<IResult> ActivateRevision(
HttpContext context,
[FromRoute] string packId,
[FromRoute] int version,
[FromBody] ActivatePolicyRevisionRequest request,
IPolicyPackRepository repository,
CancellationToken cancellationToken)
{
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyActivate);
if (scopeResult is not null)
{
return scopeResult;
}
if (request is null)
{
return Results.BadRequest(new ProblemDetails
{
Title = "Invalid request",
Detail = "Request body is required.",
Status = StatusCodes.Status400BadRequest
});
}
var actorId = ResolveActorId(context);
if (actorId is null)
{
return Results.Problem("Actor identity required.", statusCode: StatusCodes.Status401Unauthorized);
}
var result = await repository.RecordActivationAsync(
packId,
version,
actorId,
DateTimeOffset.UtcNow,
request.Comment,
cancellationToken).ConfigureAwait(false);
return result.Status switch
{
PolicyActivationResultStatus.PackNotFound => Results.NotFound(new ProblemDetails
{
Title = "Policy pack not found",
Status = StatusCodes.Status404NotFound
}),
PolicyActivationResultStatus.RevisionNotFound => Results.NotFound(new ProblemDetails
{
Title = "Policy revision not found",
Status = StatusCodes.Status404NotFound
}),
PolicyActivationResultStatus.NotApproved => Results.BadRequest(new ProblemDetails
{
Title = "Revision not approved",
Detail = "Only approved revisions may be activated.",
Status = StatusCodes.Status400BadRequest
}),
PolicyActivationResultStatus.DuplicateApproval => Results.BadRequest(new ProblemDetails
{
Title = "Approval already recorded",
Detail = "This approver has already approved activation.",
Status = StatusCodes.Status400BadRequest
}),
PolicyActivationResultStatus.PendingSecondApproval => Results.Accepted(
$"/api/policy/packs/{packId}/revisions/{version}",
new PolicyRevisionActivationResponse("pending_second_approval", PolicyPackMapper.ToDto(packId, result.Revision!))),
PolicyActivationResultStatus.Activated => Results.Ok(new PolicyRevisionActivationResponse("activated", PolicyPackMapper.ToDto(packId, result.Revision!))),
PolicyActivationResultStatus.AlreadyActive => Results.Ok(new PolicyRevisionActivationResponse("already_active", PolicyPackMapper.ToDto(packId, result.Revision!))),
_ => Results.BadRequest(new ProblemDetails
{
Title = "Activation failed",
Detail = "Unknown activation result.",
Status = StatusCodes.Status400BadRequest
})
};
}
private static string? ResolveActorId(HttpContext context)
{
var user = context.User;
var actor = user?.FindFirst(ClaimTypes.NameIdentifier)?.Value
?? user?.FindFirst(ClaimTypes.Upn)?.Value
?? user?.FindFirst("sub")?.Value;
if (!string.IsNullOrWhiteSpace(actor))
{
return actor;
}
if (context.Request.Headers.TryGetValue("X-StellaOps-Actor", out var header) && !string.IsNullOrWhiteSpace(header))
{
return header.ToString();
}
return null;
}
}
internal static class PolicyPackMapper
{
public static PolicyPackDto ToDto(PolicyPackRecord record)
=> new(record.PackId, record.DisplayName, record.CreatedAt, record.GetRevisions().Select(r => ToDto(record.PackId, r)).ToArray());
public static PolicyPackSummaryDto ToSummaryDto(PolicyPackRecord record)
=> new(record.PackId, record.DisplayName, record.CreatedAt, record.GetRevisions().Select(r => r.Version).ToArray());
public static PolicyRevisionDto ToDto(string packId, PolicyRevisionRecord revision)
=> new(
packId,
revision.Version,
revision.Status.ToString(),
revision.RequiresTwoPersonApproval,
revision.CreatedAt,
revision.ActivatedAt,
revision.Approvals.Select(a => new PolicyActivationApprovalDto(a.ActorId, a.ApprovedAt, a.Comment)).ToArray());
}
internal sealed record CreatePolicyPackRequest(string? PackId, string? DisplayName);
internal sealed record PolicyPackDto(string PackId, string? DisplayName, DateTimeOffset CreatedAt, IReadOnlyList<PolicyRevisionDto> Revisions);
internal sealed record PolicyPackSummaryDto(string PackId, string? DisplayName, DateTimeOffset CreatedAt, IReadOnlyList<int> Versions);
internal sealed record CreatePolicyRevisionRequest(int? Version, bool RequiresTwoPersonApproval, PolicyRevisionStatus InitialStatus = PolicyRevisionStatus.Approved);
internal sealed record PolicyRevisionDto(string PackId, int Version, string Status, bool RequiresTwoPersonApproval, DateTimeOffset CreatedAt, DateTimeOffset? ActivatedAt, IReadOnlyList<PolicyActivationApprovalDto> Approvals);
internal sealed record PolicyActivationApprovalDto(string ActorId, DateTimeOffset ApprovedAt, string? Comment);
internal sealed record ActivatePolicyRevisionRequest(string? Comment);
internal sealed record PolicyRevisionActivationResponse(string Status, PolicyRevisionDto Revision);