up
This commit is contained in:
@@ -0,0 +1,360 @@
|
||||
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.Overrides;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Endpoints;
|
||||
|
||||
internal static class OverrideEndpoints
|
||||
{
|
||||
public static IEndpointRouteBuilder MapOverrides(this IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
var group = endpoints.MapGroup("/api/risk/overrides")
|
||||
.RequireAuthorization()
|
||||
.WithTags("Risk Overrides");
|
||||
|
||||
group.MapPost("/", CreateOverride)
|
||||
.WithName("CreateOverride")
|
||||
.WithSummary("Create a new override with audit metadata.")
|
||||
.Produces<OverrideResponse>(StatusCodes.Status201Created)
|
||||
.Produces<ProblemHttpResult>(StatusCodes.Status400BadRequest);
|
||||
|
||||
group.MapGet("/{overrideId}", GetOverride)
|
||||
.WithName("GetOverride")
|
||||
.WithSummary("Get an override by ID.")
|
||||
.Produces<OverrideResponse>(StatusCodes.Status200OK)
|
||||
.Produces<ProblemHttpResult>(StatusCodes.Status404NotFound);
|
||||
|
||||
group.MapDelete("/{overrideId}", DeleteOverride)
|
||||
.WithName("DeleteOverride")
|
||||
.WithSummary("Delete an override.")
|
||||
.Produces(StatusCodes.Status204NoContent)
|
||||
.Produces<ProblemHttpResult>(StatusCodes.Status404NotFound);
|
||||
|
||||
group.MapGet("/profile/{profileId}", ListProfileOverrides)
|
||||
.WithName("ListProfileOverrides")
|
||||
.WithSummary("List all overrides for a risk profile.")
|
||||
.Produces<OverrideListResponse>(StatusCodes.Status200OK);
|
||||
|
||||
group.MapPost("/validate", ValidateOverride)
|
||||
.WithName("ValidateOverride")
|
||||
.WithSummary("Validate an override for conflicts before creating.")
|
||||
.Produces<OverrideValidationResponse>(StatusCodes.Status200OK);
|
||||
|
||||
group.MapPost("/{overrideId}:approve", ApproveOverride)
|
||||
.WithName("ApproveOverride")
|
||||
.WithSummary("Approve an override that requires review.")
|
||||
.Produces<OverrideResponse>(StatusCodes.Status200OK)
|
||||
.Produces<ProblemHttpResult>(StatusCodes.Status400BadRequest)
|
||||
.Produces<ProblemHttpResult>(StatusCodes.Status404NotFound);
|
||||
|
||||
group.MapPost("/{overrideId}:disable", DisableOverride)
|
||||
.WithName("DisableOverride")
|
||||
.WithSummary("Disable an active override.")
|
||||
.Produces<OverrideResponse>(StatusCodes.Status200OK)
|
||||
.Produces<ProblemHttpResult>(StatusCodes.Status404NotFound);
|
||||
|
||||
group.MapGet("/{overrideId}/history", GetOverrideHistory)
|
||||
.WithName("GetOverrideHistory")
|
||||
.WithSummary("Get application history for an override.")
|
||||
.Produces<OverrideHistoryResponse>(StatusCodes.Status200OK);
|
||||
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
private static IResult CreateOverride(
|
||||
HttpContext context,
|
||||
[FromBody] CreateOverrideRequest request,
|
||||
OverrideService overrideService,
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Reason))
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "Invalid request",
|
||||
Detail = "Reason is required for audit purposes.",
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
// Validate for conflicts
|
||||
var validation = overrideService.ValidateConflicts(request);
|
||||
if (validation.HasConflicts)
|
||||
{
|
||||
var conflictDetails = string.Join("; ", validation.Conflicts.Select(c => c.Description));
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "Override conflicts detected",
|
||||
Detail = conflictDetails,
|
||||
Status = StatusCodes.Status400BadRequest,
|
||||
Extensions = { ["conflicts"] = validation.Conflicts }
|
||||
});
|
||||
}
|
||||
|
||||
var actorId = ResolveActorId(context);
|
||||
|
||||
try
|
||||
{
|
||||
var auditedOverride = overrideService.Create(request, actorId);
|
||||
|
||||
return Results.Created(
|
||||
$"/api/risk/overrides/{auditedOverride.OverrideId}",
|
||||
new OverrideResponse(auditedOverride, validation.Warnings));
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "Invalid request",
|
||||
Detail = ex.Message,
|
||||
Status = StatusCodes.Status400BadRequest
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static IResult GetOverride(
|
||||
HttpContext context,
|
||||
[FromRoute] string overrideId,
|
||||
OverrideService overrideService)
|
||||
{
|
||||
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead);
|
||||
if (scopeResult is not null)
|
||||
{
|
||||
return scopeResult;
|
||||
}
|
||||
|
||||
var auditedOverride = overrideService.Get(overrideId);
|
||||
if (auditedOverride == null)
|
||||
{
|
||||
return Results.NotFound(new ProblemDetails
|
||||
{
|
||||
Title = "Override not found",
|
||||
Detail = $"Override '{overrideId}' was not found.",
|
||||
Status = StatusCodes.Status404NotFound
|
||||
});
|
||||
}
|
||||
|
||||
return Results.Ok(new OverrideResponse(auditedOverride, null));
|
||||
}
|
||||
|
||||
private static IResult DeleteOverride(
|
||||
HttpContext context,
|
||||
[FromRoute] string overrideId,
|
||||
OverrideService overrideService)
|
||||
{
|
||||
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyEdit);
|
||||
if (scopeResult is not null)
|
||||
{
|
||||
return scopeResult;
|
||||
}
|
||||
|
||||
if (!overrideService.Delete(overrideId))
|
||||
{
|
||||
return Results.NotFound(new ProblemDetails
|
||||
{
|
||||
Title = "Override not found",
|
||||
Detail = $"Override '{overrideId}' was not found.",
|
||||
Status = StatusCodes.Status404NotFound
|
||||
});
|
||||
}
|
||||
|
||||
return Results.NoContent();
|
||||
}
|
||||
|
||||
private static IResult ListProfileOverrides(
|
||||
HttpContext context,
|
||||
[FromRoute] string profileId,
|
||||
[FromQuery] bool includeInactive,
|
||||
OverrideService overrideService)
|
||||
{
|
||||
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead);
|
||||
if (scopeResult is not null)
|
||||
{
|
||||
return scopeResult;
|
||||
}
|
||||
|
||||
var overrides = overrideService.ListByProfile(profileId, includeInactive);
|
||||
|
||||
return Results.Ok(new OverrideListResponse(profileId, overrides));
|
||||
}
|
||||
|
||||
private static IResult ValidateOverride(
|
||||
HttpContext context,
|
||||
[FromBody] CreateOverrideRequest request,
|
||||
OverrideService overrideService)
|
||||
{
|
||||
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead);
|
||||
if (scopeResult is not null)
|
||||
{
|
||||
return scopeResult;
|
||||
}
|
||||
|
||||
if (request == null)
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "Invalid request",
|
||||
Detail = "Request body is required.",
|
||||
Status = StatusCodes.Status400BadRequest
|
||||
});
|
||||
}
|
||||
|
||||
var validation = overrideService.ValidateConflicts(request);
|
||||
|
||||
return Results.Ok(new OverrideValidationResponse(validation));
|
||||
}
|
||||
|
||||
private static IResult ApproveOverride(
|
||||
HttpContext context,
|
||||
[FromRoute] string overrideId,
|
||||
OverrideService overrideService)
|
||||
{
|
||||
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyActivate);
|
||||
if (scopeResult is not null)
|
||||
{
|
||||
return scopeResult;
|
||||
}
|
||||
|
||||
var actorId = ResolveActorId(context);
|
||||
|
||||
try
|
||||
{
|
||||
var auditedOverride = overrideService.Approve(overrideId, actorId ?? "system");
|
||||
if (auditedOverride == null)
|
||||
{
|
||||
return Results.NotFound(new ProblemDetails
|
||||
{
|
||||
Title = "Override not found",
|
||||
Detail = $"Override '{overrideId}' was not found.",
|
||||
Status = StatusCodes.Status404NotFound
|
||||
});
|
||||
}
|
||||
|
||||
return Results.Ok(new OverrideResponse(auditedOverride, null));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "Approval failed",
|
||||
Detail = ex.Message,
|
||||
Status = StatusCodes.Status400BadRequest
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static IResult DisableOverride(
|
||||
HttpContext context,
|
||||
[FromRoute] string overrideId,
|
||||
[FromQuery] string? reason,
|
||||
OverrideService overrideService)
|
||||
{
|
||||
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyEdit);
|
||||
if (scopeResult is not null)
|
||||
{
|
||||
return scopeResult;
|
||||
}
|
||||
|
||||
var actorId = ResolveActorId(context);
|
||||
|
||||
var auditedOverride = overrideService.Disable(overrideId, actorId ?? "system", reason);
|
||||
if (auditedOverride == null)
|
||||
{
|
||||
return Results.NotFound(new ProblemDetails
|
||||
{
|
||||
Title = "Override not found",
|
||||
Detail = $"Override '{overrideId}' was not found.",
|
||||
Status = StatusCodes.Status404NotFound
|
||||
});
|
||||
}
|
||||
|
||||
return Results.Ok(new OverrideResponse(auditedOverride, null));
|
||||
}
|
||||
|
||||
private static IResult GetOverrideHistory(
|
||||
HttpContext context,
|
||||
[FromRoute] string overrideId,
|
||||
[FromQuery] int limit,
|
||||
OverrideService overrideService)
|
||||
{
|
||||
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead);
|
||||
if (scopeResult is not null)
|
||||
{
|
||||
return scopeResult;
|
||||
}
|
||||
|
||||
var effectiveLimit = limit > 0 ? limit : 100;
|
||||
var history = overrideService.GetApplicationHistory(overrideId, effectiveLimit);
|
||||
|
||||
return Results.Ok(new OverrideHistoryResponse(overrideId, history));
|
||||
}
|
||||
|
||||
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 OverrideResponse(
|
||||
AuditedOverride Override,
|
||||
IReadOnlyList<string>? Warnings);
|
||||
|
||||
internal sealed record OverrideListResponse(
|
||||
string ProfileId,
|
||||
IReadOnlyList<AuditedOverride> Overrides);
|
||||
|
||||
internal sealed record OverrideValidationResponse(OverrideConflictValidation Validation);
|
||||
|
||||
internal sealed record OverrideHistoryResponse(
|
||||
string OverrideId,
|
||||
IReadOnlyList<OverrideApplicationRecord> History);
|
||||
|
||||
#endregion
|
||||
Reference in New Issue
Block a user