up
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Policy.Engine.Services;
|
||||
using StellaOps.Policy.RiskProfile.Export;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Endpoints;
|
||||
|
||||
internal static class ProfileExportEndpoints
|
||||
{
|
||||
public static IEndpointRouteBuilder MapProfileExport(this IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
var group = endpoints.MapGroup("/api/risk/profiles/export")
|
||||
.RequireAuthorization()
|
||||
.WithTags("Profile Export/Import");
|
||||
|
||||
group.MapPost("/", ExportProfiles)
|
||||
.WithName("ExportProfiles")
|
||||
.WithSummary("Export risk profiles as a signed bundle.")
|
||||
.Produces<ExportResponse>(StatusCodes.Status200OK)
|
||||
.Produces<ProblemHttpResult>(StatusCodes.Status400BadRequest);
|
||||
|
||||
group.MapPost("/download", DownloadBundle)
|
||||
.WithName("DownloadProfileBundle")
|
||||
.WithSummary("Export and download risk profiles as a JSON file.")
|
||||
.Produces<FileContentHttpResult>(StatusCodes.Status200OK, contentType: "application/json");
|
||||
|
||||
endpoints.MapPost("/api/risk/profiles/import", ImportProfiles)
|
||||
.RequireAuthorization()
|
||||
.WithName("ImportProfiles")
|
||||
.WithSummary("Import risk profiles from a signed bundle.")
|
||||
.WithTags("Profile Export/Import")
|
||||
.Produces<ImportResponse>(StatusCodes.Status200OK)
|
||||
.Produces<ProblemHttpResult>(StatusCodes.Status400BadRequest);
|
||||
|
||||
endpoints.MapPost("/api/risk/profiles/verify", VerifyBundle)
|
||||
.RequireAuthorization()
|
||||
.WithName("VerifyProfileBundle")
|
||||
.WithSummary("Verify the signature of a profile bundle without importing.")
|
||||
.WithTags("Profile Export/Import")
|
||||
.Produces<VerifyResponse>(StatusCodes.Status200OK);
|
||||
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
private static IResult ExportProfiles(
|
||||
HttpContext context,
|
||||
[FromBody] ExportProfilesRequest request,
|
||||
RiskProfileConfigurationService profileService,
|
||||
ProfileExportService exportService)
|
||||
{
|
||||
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead);
|
||||
if (scopeResult is not null)
|
||||
{
|
||||
return scopeResult;
|
||||
}
|
||||
|
||||
if (request == null || request.ProfileIds == null || request.ProfileIds.Count == 0)
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "Invalid request",
|
||||
Detail = "At least one profile ID is required.",
|
||||
Status = StatusCodes.Status400BadRequest
|
||||
});
|
||||
}
|
||||
|
||||
var profiles = new List<StellaOps.Policy.RiskProfile.Models.RiskProfileModel>();
|
||||
var notFound = new List<string>();
|
||||
|
||||
foreach (var profileId in request.ProfileIds)
|
||||
{
|
||||
var profile = profileService.GetProfile(profileId);
|
||||
if (profile != null)
|
||||
{
|
||||
profiles.Add(profile);
|
||||
}
|
||||
else
|
||||
{
|
||||
notFound.Add(profileId);
|
||||
}
|
||||
}
|
||||
|
||||
if (notFound.Count > 0)
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "Profiles not found",
|
||||
Detail = $"The following profiles were not found: {string.Join(", ", notFound)}",
|
||||
Status = StatusCodes.Status400BadRequest
|
||||
});
|
||||
}
|
||||
|
||||
var actorId = ResolveActorId(context);
|
||||
var bundle = exportService.Export(profiles, request, actorId);
|
||||
|
||||
return Results.Ok(new ExportResponse(bundle));
|
||||
}
|
||||
|
||||
private static IResult DownloadBundle(
|
||||
HttpContext context,
|
||||
[FromBody] ExportProfilesRequest request,
|
||||
RiskProfileConfigurationService profileService,
|
||||
ProfileExportService exportService)
|
||||
{
|
||||
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead);
|
||||
if (scopeResult is not null)
|
||||
{
|
||||
return scopeResult;
|
||||
}
|
||||
|
||||
if (request == null || request.ProfileIds == null || request.ProfileIds.Count == 0)
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "Invalid request",
|
||||
Detail = "At least one profile ID is required.",
|
||||
Status = StatusCodes.Status400BadRequest
|
||||
});
|
||||
}
|
||||
|
||||
var profiles = new List<StellaOps.Policy.RiskProfile.Models.RiskProfileModel>();
|
||||
|
||||
foreach (var profileId in request.ProfileIds)
|
||||
{
|
||||
var profile = profileService.GetProfile(profileId);
|
||||
if (profile != null)
|
||||
{
|
||||
profiles.Add(profile);
|
||||
}
|
||||
}
|
||||
|
||||
var actorId = ResolveActorId(context);
|
||||
var bundle = exportService.Export(profiles, request, actorId);
|
||||
var json = exportService.SerializeBundle(bundle);
|
||||
var bytes = System.Text.Encoding.UTF8.GetBytes(json);
|
||||
|
||||
var fileName = $"risk-profiles-{bundle.BundleId}.json";
|
||||
return Results.File(bytes, "application/json", fileName);
|
||||
}
|
||||
|
||||
private static IResult ImportProfiles(
|
||||
HttpContext context,
|
||||
[FromBody] ImportProfilesRequest request,
|
||||
RiskProfileConfigurationService profileService,
|
||||
ProfileExportService exportService)
|
||||
{
|
||||
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyEdit);
|
||||
if (scopeResult is not null)
|
||||
{
|
||||
return scopeResult;
|
||||
}
|
||||
|
||||
if (request == null || request.Bundle == null)
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "Invalid request",
|
||||
Detail = "Bundle is required.",
|
||||
Status = StatusCodes.Status400BadRequest
|
||||
});
|
||||
}
|
||||
|
||||
var actorId = ResolveActorId(context);
|
||||
|
||||
// Create an export service with save capability
|
||||
var importExportService = new ProfileExportService(
|
||||
timeProvider: TimeProvider.System,
|
||||
profileLookup: id => profileService.GetProfile(id),
|
||||
lifecycleLookup: null,
|
||||
profileSave: profile => profileService.RegisterProfile(profile),
|
||||
keyLookup: null);
|
||||
|
||||
var result = importExportService.Import(request, actorId);
|
||||
|
||||
return Results.Ok(new ImportResponse(result));
|
||||
}
|
||||
|
||||
private static IResult VerifyBundle(
|
||||
HttpContext context,
|
||||
[FromBody] RiskProfileBundle bundle,
|
||||
ProfileExportService exportService)
|
||||
{
|
||||
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead);
|
||||
if (scopeResult is not null)
|
||||
{
|
||||
return scopeResult;
|
||||
}
|
||||
|
||||
if (bundle == null)
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "Invalid request",
|
||||
Detail = "Bundle is required.",
|
||||
Status = StatusCodes.Status400BadRequest
|
||||
});
|
||||
}
|
||||
|
||||
var verification = exportService.VerifySignature(bundle);
|
||||
|
||||
return Results.Ok(new VerifyResponse(verification, bundle.Metadata));
|
||||
}
|
||||
|
||||
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 ExportResponse(RiskProfileBundle Bundle);
|
||||
|
||||
internal sealed record ImportResponse(ImportResult Result);
|
||||
|
||||
internal sealed record VerifyResponse(
|
||||
SignatureVerificationResult Verification,
|
||||
BundleMetadata Metadata);
|
||||
|
||||
#endregion
|
||||
Reference in New Issue
Block a user