Files
git.stella-ops.org/src/Policy/StellaOps.Policy.Engine/Endpoints/ScopeAttachmentEndpoints.cs
StellaOps Bot 3b96b2e3ea
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
up
2025-11-27 23:45:09 +02:00

291 lines
9.8 KiB
C#

using System.Security.Claims;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using StellaOps.Auth.Abstractions;
using StellaOps.Policy.Engine.Services;
using StellaOps.Policy.RiskProfile.Scope;
namespace StellaOps.Policy.Engine.Endpoints;
internal static class ScopeAttachmentEndpoints
{
public static IEndpointRouteBuilder MapScopeAttachments(this IEndpointRouteBuilder endpoints)
{
var group = endpoints.MapGroup("/api/risk/scopes")
.RequireAuthorization()
.WithTags("Risk Profile Scopes");
group.MapPost("/attachments", CreateAttachment)
.WithName("CreateScopeAttachment")
.WithSummary("Attach a risk profile to a scope (organization, project, environment, or component).")
.Produces<ScopeAttachmentResponse>(StatusCodes.Status201Created)
.Produces<ProblemHttpResult>(StatusCodes.Status400BadRequest);
group.MapGet("/attachments/{attachmentId}", GetAttachment)
.WithName("GetScopeAttachment")
.WithSummary("Get a scope attachment by ID.")
.Produces<ScopeAttachmentResponse>(StatusCodes.Status200OK)
.Produces<ProblemHttpResult>(StatusCodes.Status404NotFound);
group.MapDelete("/attachments/{attachmentId}", DeleteAttachment)
.WithName("DeleteScopeAttachment")
.WithSummary("Delete a scope attachment.")
.Produces(StatusCodes.Status204NoContent)
.Produces<ProblemHttpResult>(StatusCodes.Status404NotFound);
group.MapPost("/attachments/{attachmentId}:expire", ExpireAttachment)
.WithName("ExpireScopeAttachment")
.WithSummary("Expire a scope attachment immediately.")
.Produces<ScopeAttachmentResponse>(StatusCodes.Status200OK)
.Produces<ProblemHttpResult>(StatusCodes.Status404NotFound);
group.MapGet("/attachments", ListAttachments)
.WithName("ListScopeAttachments")
.WithSummary("List scope attachments with optional filtering.")
.Produces<ScopeAttachmentListResponse>(StatusCodes.Status200OK);
group.MapPost("/resolve", ResolveScope)
.WithName("ResolveScope")
.WithSummary("Resolve the effective risk profile for a given scope selector.")
.Produces<ScopeResolutionResponse>(StatusCodes.Status200OK);
group.MapGet("/{scopeType}/{scopeId}/attachments", GetScopeAttachments)
.WithName("GetScopeAttachments")
.WithSummary("Get all attachments for a specific scope.")
.Produces<ScopeAttachmentListResponse>(StatusCodes.Status200OK);
return endpoints;
}
private static IResult CreateAttachment(
HttpContext context,
[FromBody] CreateScopeAttachmentRequest request,
ScopeAttachmentService attachmentService,
RiskProfileConfigurationService profileService)
{
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyEdit);
if (scopeResult is not null)
{
return scopeResult;
}
if (request == null || string.IsNullOrWhiteSpace(request.ProfileId))
{
return Results.BadRequest(new ProblemDetails
{
Title = "Invalid request",
Detail = "ProfileId is required.",
Status = StatusCodes.Status400BadRequest
});
}
// Verify profile exists
var profile = profileService.GetProfile(request.ProfileId);
if (profile == null)
{
return Results.BadRequest(new ProblemDetails
{
Title = "Profile not found",
Detail = $"Risk profile '{request.ProfileId}' was not found.",
Status = StatusCodes.Status400BadRequest
});
}
var actorId = ResolveActorId(context);
try
{
var attachment = attachmentService.Create(request, actorId);
return Results.Created(
$"/api/risk/scopes/attachments/{attachment.Id}",
new ScopeAttachmentResponse(attachment));
}
catch (ArgumentException ex)
{
return Results.BadRequest(new ProblemDetails
{
Title = "Invalid request",
Detail = ex.Message,
Status = StatusCodes.Status400BadRequest
});
}
}
private static IResult GetAttachment(
HttpContext context,
[FromRoute] string attachmentId,
ScopeAttachmentService attachmentService)
{
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead);
if (scopeResult is not null)
{
return scopeResult;
}
var attachment = attachmentService.Get(attachmentId);
if (attachment == null)
{
return Results.NotFound(new ProblemDetails
{
Title = "Attachment not found",
Detail = $"Scope attachment '{attachmentId}' was not found.",
Status = StatusCodes.Status404NotFound
});
}
return Results.Ok(new ScopeAttachmentResponse(attachment));
}
private static IResult DeleteAttachment(
HttpContext context,
[FromRoute] string attachmentId,
ScopeAttachmentService attachmentService)
{
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyEdit);
if (scopeResult is not null)
{
return scopeResult;
}
if (!attachmentService.Delete(attachmentId))
{
return Results.NotFound(new ProblemDetails
{
Title = "Attachment not found",
Detail = $"Scope attachment '{attachmentId}' was not found.",
Status = StatusCodes.Status404NotFound
});
}
return Results.NoContent();
}
private static IResult ExpireAttachment(
HttpContext context,
[FromRoute] string attachmentId,
ScopeAttachmentService attachmentService)
{
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyEdit);
if (scopeResult is not null)
{
return scopeResult;
}
var actorId = ResolveActorId(context);
var attachment = attachmentService.Expire(attachmentId, actorId);
if (attachment == null)
{
return Results.NotFound(new ProblemDetails
{
Title = "Attachment not found",
Detail = $"Scope attachment '{attachmentId}' was not found.",
Status = StatusCodes.Status404NotFound
});
}
return Results.Ok(new ScopeAttachmentResponse(attachment));
}
private static IResult ListAttachments(
HttpContext context,
[FromQuery] ScopeType? scopeType,
[FromQuery] string? scopeId,
[FromQuery] string? profileId,
[FromQuery] bool includeExpired,
[FromQuery] int limit,
ScopeAttachmentService attachmentService)
{
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead);
if (scopeResult is not null)
{
return scopeResult;
}
var query = new ScopeAttachmentQuery(
ScopeType: scopeType,
ScopeId: scopeId,
ProfileId: profileId,
IncludeExpired: includeExpired,
Limit: limit > 0 ? limit : 100);
var attachments = attachmentService.Query(query);
return Results.Ok(new ScopeAttachmentListResponse(attachments));
}
private static IResult ResolveScope(
HttpContext context,
[FromBody] ScopeSelector selector,
ScopeAttachmentService attachmentService)
{
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead);
if (scopeResult is not null)
{
return scopeResult;
}
if (selector == null)
{
return Results.BadRequest(new ProblemDetails
{
Title = "Invalid request",
Detail = "Scope selector is required.",
Status = StatusCodes.Status400BadRequest
});
}
var result = attachmentService.Resolve(selector);
return Results.Ok(new ScopeResolutionResponse(result));
}
private static IResult GetScopeAttachments(
HttpContext context,
[FromRoute] ScopeType scopeType,
[FromRoute] string scopeId,
ScopeAttachmentService attachmentService)
{
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead);
if (scopeResult is not null)
{
return scopeResult;
}
var attachments = attachmentService.GetAttachmentsForScope(scopeType, scopeId);
return Results.Ok(new ScopeAttachmentListResponse(attachments));
}
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;
}
}
#region Response DTOs
internal sealed record ScopeAttachmentResponse(ScopeAttachment Attachment);
internal sealed record ScopeAttachmentListResponse(IReadOnlyList<ScopeAttachment> Attachments);
internal sealed record ScopeResolutionResponse(ScopeResolutionResult Result);
#endregion