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>
|
||||
Reference in New Issue
Block a user