291 lines
9.8 KiB
C#
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
|