up
Some checks failed
api-governance / spectral-lint (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-26 20:23:28 +02:00
parent 4831c7fcb0
commit d63af51f84
139 changed files with 8010 additions and 2795 deletions

View File

@@ -0,0 +1,108 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using StellaOps.AirGap.Controller.Endpoints.Contracts;
using StellaOps.AirGap.Controller.Services;
using StellaOps.AirGap.Time.Models;
using StellaOps.AirGap.Time.Services;
namespace StellaOps.AirGap.Controller.Endpoints;
internal static class AirGapEndpoints
{
private const string StatusScope = "airgap:status:read";
private const string SealScope = "airgap:seal";
public static RouteGroupBuilder MapAirGapEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/system/airgap")
.RequireAuthorization();
group.MapGet("/status", HandleStatus)
.RequireScope(StatusScope)
.WithName("AirGapStatus");
group.MapPost("/seal", HandleSeal)
.RequireScope(SealScope)
.WithName("AirGapSeal");
group.MapPost("/unseal", HandleUnseal)
.RequireScope(SealScope)
.WithName("AirGapUnseal");
return group;
}
private static async Task<IResult> HandleStatus(
ClaimsPrincipal user,
AirGapStateService service,
TimeProvider timeProvider,
HttpContext httpContext,
CancellationToken cancellationToken)
{
var tenantId = ResolveTenant(httpContext);
var status = await service.GetStatusAsync(tenantId, timeProvider.GetUtcNow(), cancellationToken);
return Results.Ok(AirGapStatusResponse.FromStatus(status));
}
private static async Task<IResult> HandleSeal(
SealRequest request,
ClaimsPrincipal user,
AirGapStateService service,
StalenessCalculator stalenessCalculator,
TimeProvider timeProvider,
HttpContext httpContext,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(request.PolicyHash))
{
return Results.BadRequest(new { error = "policy_hash_required" });
}
var tenantId = ResolveTenant(httpContext);
var anchor = request.TimeAnchor ?? TimeAnchor.Unknown;
var budget = request.StalenessBudget ?? StalenessBudget.Default;
var now = timeProvider.GetUtcNow();
var state = await service.SealAsync(tenantId, request.PolicyHash!, anchor, budget, now, cancellationToken);
var status = new AirGapStatus(state, stalenessCalculator.Evaluate(anchor, budget, now), now);
return Results.Ok(AirGapStatusResponse.FromStatus(status));
}
private static async Task<IResult> HandleUnseal(
ClaimsPrincipal user,
AirGapStateService service,
TimeProvider timeProvider,
HttpContext httpContext,
CancellationToken cancellationToken)
{
var tenantId = ResolveTenant(httpContext);
var state = await service.UnsealAsync(tenantId, timeProvider.GetUtcNow(), cancellationToken);
var status = new AirGapStatus(state, StalenessEvaluation.Unknown, timeProvider.GetUtcNow());
return Results.Ok(AirGapStatusResponse.FromStatus(status));
}
private static string ResolveTenant(HttpContext httpContext)
{
if (httpContext.Request.Headers.TryGetValue("x-tenant-id", out var tenantHeader) && !string.IsNullOrWhiteSpace(tenantHeader))
{
return tenantHeader.ToString();
}
return "default";
}
}
internal static class AuthorizationExtensions
{
public static RouteHandlerBuilder RequireScope(this RouteHandlerBuilder builder, string requiredScope)
{
return builder.RequireAuthorization(policy =>
{
policy.RequireAssertion(ctx =>
{
var scopes = ctx.User.FindFirstValue("scope") ?? ctx.User.FindFirstValue("scp") ?? string.Empty;
return scopes.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Contains(requiredScope, StringComparer.OrdinalIgnoreCase);
});
});
}
}

View File

@@ -0,0 +1,25 @@
using StellaOps.AirGap.Controller.Domain;
using StellaOps.AirGap.Controller.Services;
using StellaOps.AirGap.Time.Models;
namespace StellaOps.AirGap.Controller.Endpoints.Contracts;
public sealed record AirGapStatusResponse(
string TenantId,
bool Sealed,
string? PolicyHash,
TimeAnchor TimeAnchor,
StalenessEvaluation Staleness,
DateTimeOffset LastTransitionAt,
DateTimeOffset EvaluatedAt)
{
public static AirGapStatusResponse FromStatus(AirGapStatus status) =>
new(
status.State.TenantId,
status.State.Sealed,
status.State.PolicyHash,
status.State.TimeAnchor,
status.Staleness,
status.State.LastTransitionAt,
status.EvaluatedAt);
}

View File

@@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
using StellaOps.AirGap.Time.Models;
namespace StellaOps.AirGap.Controller.Endpoints.Contracts;
public sealed class SealRequest
{
[Required]
public string? PolicyHash { get; set; }
public TimeAnchor? TimeAnchor { get; set; }
public StalenessBudget? StalenessBudget { get; set; }
}