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:
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user