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(StatusCodes.Status201Created) .Produces(StatusCodes.Status400BadRequest); group.MapGet("/attachments/{attachmentId}", GetAttachment) .WithName("GetScopeAttachment") .WithSummary("Get a scope attachment by ID.") .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound); group.MapDelete("/attachments/{attachmentId}", DeleteAttachment) .WithName("DeleteScopeAttachment") .WithSummary("Delete a scope attachment.") .Produces(StatusCodes.Status204NoContent) .Produces(StatusCodes.Status404NotFound); group.MapPost("/attachments/{attachmentId}:expire", ExpireAttachment) .WithName("ExpireScopeAttachment") .WithSummary("Expire a scope attachment immediately.") .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound); group.MapGet("/attachments", ListAttachments) .WithName("ListScopeAttachments") .WithSummary("List scope attachments with optional filtering.") .Produces(StatusCodes.Status200OK); group.MapPost("/resolve", ResolveScope) .WithName("ResolveScope") .WithSummary("Resolve the effective risk profile for a given scope selector.") .Produces(StatusCodes.Status200OK); group.MapGet("/{scopeType}/{scopeId}/attachments", GetScopeAttachments) .WithName("GetScopeAttachments") .WithSummary("Get all attachments for a specific scope.") .Produces(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 Attachments); internal sealed record ScopeResolutionResponse(ScopeResolutionResult Result); #endregion