Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Implemented comprehensive unit tests for RabbitMqTransportServer, covering constructor, disposal, connection management, event handlers, and exception handling. - Added configuration tests for RabbitMqTransportServer to validate SSL, durable queues, auto-recovery, and custom virtual host options. - Created unit tests for UdpFrameProtocol, including frame parsing and serialization, header size validation, and round-trip data preservation. - Developed tests for UdpTransportClient, focusing on connection handling, event subscriptions, and exception scenarios. - Established tests for UdpTransportServer, ensuring proper start/stop behavior, connection state management, and event handling. - Included tests for UdpTransportOptions to verify default values and modification capabilities. - Enhanced service registration tests for Udp transport services in the dependency injection container.
242 lines
8.0 KiB
C#
242 lines
8.0 KiB
C#
using System.Security.Claims;
|
|
using System.Text.Json;
|
|
using Microsoft.AspNetCore.Http.HttpResults;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using StellaOps.Auth.Abstractions;
|
|
using StellaOps.Cryptography;
|
|
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,
|
|
ICryptoHash cryptoHash)
|
|
{
|
|
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(
|
|
cryptoHash: cryptoHash,
|
|
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
|