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,107 @@
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using StellaOps.Policy;
using StellaOps.Policy.Engine.Services;
using System.Collections.Immutable;
namespace StellaOps.Policy.Engine.Endpoints;
internal static class PolicyCompilationEndpoints
{
private const string CompileRoute = "/api/policy/policies/{policyId}/versions/{version}:compile";
public static IEndpointRouteBuilder MapPolicyCompilation(this IEndpointRouteBuilder endpoints)
{
endpoints.MapPost(CompileRoute, CompilePolicy)
.WithName("CompilePolicy")
.WithSummary("Compile and lint a policy DSL document.")
.WithDescription("Compiles a stella-dsl@1 policy document and returns deterministic digest and statistics.")
.Produces<PolicyCompileResponse>(StatusCodes.Status200OK)
.Produces<ProblemHttpResult>(StatusCodes.Status400BadRequest)
.RequireAuthorization(); // scopes enforced by policy middleware.
return endpoints;
}
private static IResult CompilePolicy(
[FromRoute] string policyId,
[FromRoute] int version,
[FromBody] PolicyCompileRequest request,
PolicyCompilationService compilationService)
{
if (request is null)
{
return Results.BadRequest(BuildProblem("ERR_POL_001", "Request body missing.", policyId, version));
}
var result = compilationService.Compile(request);
if (!result.Success)
{
return Results.BadRequest(BuildProblem("ERR_POL_001", "Policy compilation failed.", policyId, version, result.Diagnostics));
}
var response = new PolicyCompileResponse(
result.Digest!,
result.Statistics ?? new PolicyCompilationStatistics(0, ImmutableDictionary<string, int>.Empty),
ConvertDiagnostics(result.Diagnostics));
return Results.Ok(response);
}
private static PolicyProblemDetails BuildProblem(string code, string message, string policyId, int version, ImmutableArray<PolicyIssue>? diagnostics = null)
{
var problem = new PolicyProblemDetails
{
Code = code,
Title = "Policy compilation error",
Detail = message,
PolicyId = policyId,
PolicyVersion = version
};
if (diagnostics is { Length: > 0 } diag)
{
problem.Diagnostics = diag;
}
return problem;
}
private static ImmutableArray<PolicyDiagnosticDto> ConvertDiagnostics(ImmutableArray<PolicyIssue> issues)
{
if (issues.IsDefaultOrEmpty)
{
return ImmutableArray<PolicyDiagnosticDto>.Empty;
}
var builder = ImmutableArray.CreateBuilder<PolicyDiagnosticDto>(issues.Length);
foreach (var issue in issues)
{
if (issue.Severity != PolicyIssueSeverity.Warning)
{
continue;
}
builder.Add(new PolicyDiagnosticDto(issue.Code, issue.Message, issue.Path));
}
return builder.ToImmutable();
}
private sealed class PolicyProblemDetails : ProblemDetails
{
public string Code { get; set; } = "ERR_POL_001";
public string? PolicyId { get; set; }
public int PolicyVersion { get; set; }
public ImmutableArray<PolicyIssue> Diagnostics { get; set; } = ImmutableArray<PolicyIssue>.Empty;
}
}
internal sealed record PolicyCompileResponse(
string Digest,
PolicyCompilationStatistics Statistics,
ImmutableArray<PolicyDiagnosticDto> Warnings);
internal sealed record PolicyDiagnosticDto(string Code, string Message, string Path);

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