Files
git.stella-ops.org/src/Policy/StellaOps.Policy.Engine/Endpoints/ProfileExportEndpoints.cs
master cc69d332e3
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Add unit tests for RabbitMq and Udp transport servers and clients
- 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.
2025-12-05 19:01:12 +02:00

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