feat: Document completed tasks for KMS, Cryptography, and Plugin Libraries

- Added detailed task completion records for KMS interface implementation and CLI support for file-based keys.
- Documented security enhancements including Argon2id password hashing, audit event contracts, and rate limiting configurations.
- Included scoped service support and integration updates for the Plugin platform, ensuring proper DI handling and testing coverage.
This commit is contained in:
master
2025-10-31 14:37:45 +02:00
parent 240e8ff25d
commit 15b4a1de6a
312 changed files with 6399 additions and 3319 deletions

View File

@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Http;
using HttpResults = Microsoft.AspNetCore.Http.Results;
using StellaOps.Aoc;
namespace StellaOps.Aoc.AspNetCore.Results;
/// <summary>
/// Helpers for emitting Aggregation-Only Contract error responses.
/// </summary>
public static class AocHttpResults
{
private const string DefaultProblemType = "https://stella-ops.org/problems/aoc-violation";
/// <summary>
/// Converts an <see cref="AocGuardException"/> into a RFC 7807 problem response.
/// </summary>
/// <param name="httpContext">The current HTTP context.</param>
/// <param name="exception">The guard exception.</param>
/// <param name="title">Optional problem title.</param>
/// <param name="detail">Optional problem detail.</param>
/// <param name="type">Optional problem type URI.</param>
/// <param name="status">Optional HTTP status code override.</param>
/// <param name="extensions">Optional extension payload merged with guard details.</param>
/// <returns>An HTTP result representing the problem response.</returns>
public static IResult Problem(
HttpContext httpContext,
AocGuardException exception,
string? title = null,
string? detail = null,
string? type = null,
int? status = null,
IDictionary<string, object?>? extensions = null)
{
if (httpContext is null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (exception is null)
{
throw new ArgumentNullException(nameof(exception));
}
var primaryCode = exception.Result.Violations.IsDefaultOrEmpty
? "ERR_AOC_000"
: exception.Result.Violations[0].ErrorCode;
var violationPayload = exception.Result.Violations
.Select(v => new Dictionary<string, object?>(StringComparer.Ordinal)
{
["code"] = v.ErrorCode,
["path"] = v.Path,
["message"] = v.Message
})
.ToArray();
var extensionPayload = new Dictionary<string, object?>(StringComparer.Ordinal)
{
["code"] = primaryCode,
["violations"] = violationPayload
};
if (extensions is not null)
{
foreach (var kvp in extensions)
{
extensionPayload[kvp.Key] = kvp.Value;
}
}
var statusCode = status ?? MapErrorCodeToStatus(primaryCode);
var problemType = type ?? DefaultProblemType;
var problemDetail = detail ?? $"AOC guard rejected the request with {primaryCode}.";
var problemTitle = title ?? "Aggregation-Only Contract violation";
return HttpResults.Problem(
statusCode: statusCode,
title: problemTitle,
detail: problemDetail,
type: problemType,
extensions: extensionPayload);
}
private static int MapErrorCodeToStatus(string errorCode) => errorCode switch
{
"ERR_AOC_003" => StatusCodes.Status409Conflict,
"ERR_AOC_004" => StatusCodes.Status422UnprocessableEntity,
"ERR_AOC_005" => StatusCodes.Status422UnprocessableEntity,
"ERR_AOC_006" => StatusCodes.Status403Forbidden,
_ => StatusCodes.Status400BadRequest,
};
}

View File

@@ -0,0 +1,91 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using StellaOps.Aoc;
namespace StellaOps.Aoc.AspNetCore.Routing;
public sealed class AocGuardEndpointFilter<TRequest> : IEndpointFilter
{
private readonly Func<TRequest, IEnumerable<object?>> _payloadSelector;
private readonly JsonSerializerOptions _serializerOptions;
private readonly AocGuardOptions? _guardOptions;
public AocGuardEndpointFilter(
Func<TRequest, IEnumerable<object?>> payloadSelector,
JsonSerializerOptions? serializerOptions,
AocGuardOptions? guardOptions)
{
_payloadSelector = payloadSelector ?? throw new ArgumentNullException(nameof(payloadSelector));
_serializerOptions = serializerOptions ?? new JsonSerializerOptions(JsonSerializerDefaults.Web);
_guardOptions = guardOptions;
}
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
if (TryGetArgument(context, out var request))
{
var payloads = _payloadSelector(request);
if (payloads is not null)
{
var guard = context.HttpContext.RequestServices.GetRequiredService<IAocGuard>();
var options = ResolveOptions(context.HttpContext.RequestServices);
foreach (var payload in payloads)
{
if (payload is null)
{
continue;
}
JsonElement element = payload switch
{
JsonElement jsonElement => jsonElement,
JsonDocument jsonDocument => jsonDocument.RootElement,
_ => JsonSerializer.SerializeToElement(payload, _serializerOptions)
};
guard.ValidateOrThrow(element, options);
}
}
}
return await next(context).ConfigureAwait(false);
}
private AocGuardOptions ResolveOptions(IServiceProvider services)
{
if (_guardOptions is not null)
{
return _guardOptions;
}
var options = services.GetService<IOptions<AocGuardOptions>>();
return options?.Value ?? AocGuardOptions.Default;
}
private static bool TryGetArgument(EndpointFilterInvocationContext context, out TRequest argument)
{
for (var i = 0; i < context.Arguments.Count; i++)
{
if (context.Arguments[i] is TRequest typedArgument)
{
argument = typedArgument;
return true;
}
}
argument = default!;
return false;
}
}

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Aoc\StellaOps.Aoc.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,49 @@
using System.Collections.Immutable;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Aoc;
using StellaOps.Aoc.AspNetCore.Results;
namespace StellaOps.Aoc.AspNetCore.Tests;
public sealed class AocHttpResultsTests
{
[Fact]
public async Task Problem_WritesProblemDetails_WithGuardViolations()
{
// Arrange
var violations = ImmutableArray.Create(
AocViolation.Create(AocViolationCode.MissingProvenance, "/upstream", "Missing upstream"),
AocViolation.Create(AocViolationCode.ForbiddenField, "/severity", "Forbidden"));
var result = AocGuardResult.FromViolations(violations);
var exception = new AocGuardException(result);
var context = new DefaultHttpContext();
context.Response.Body = new MemoryStream();
var services = new ServiceCollection();
services.AddLogging();
services.AddProblemDetails();
context.RequestServices = services.BuildServiceProvider();
// Act
var problem = AocHttpResults.Problem(context, exception);
await problem.ExecuteAsync(context);
context.Response.Body.Seek(0, SeekOrigin.Begin);
using var document = await JsonDocument.ParseAsync(context.Response.Body, cancellationToken: TestContext.Current.CancellationToken);
var root = document.RootElement;
// Assert
Assert.Equal(StatusCodes.Status422UnprocessableEntity, context.Response.StatusCode);
Assert.Equal("Aggregation-Only Contract violation", root.GetProperty("title").GetString());
Assert.Equal("ERR_AOC_004", root.GetProperty("code").GetString());
var violationsJson = root.GetProperty("violations");
Assert.Equal(2, violationsJson.GetArrayLength());
Assert.Equal("ERR_AOC_004", violationsJson[0].GetProperty("code").GetString());
Assert.Equal("/upstream", violationsJson[0].GetProperty("path").GetString());
}
}

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
<UseConcelierTestInfra>false</UseConcelierTestInfra>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit.v3" Version="3.0.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.3" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\__Libraries\StellaOps.Aoc.AspNetCore\StellaOps.Aoc.AspNetCore.csproj" />
</ItemGroup>
</Project>