test(altcha): runtime validation of check pipeline in Svrnty.Sample
Adds a [Altcha]-decorated ProtectedActionCommand with IHasAltchaSolution and a StubAltchaVerifier that treats the literal "valid-solution" as passing PoW. Exercises both the HTTP MinimalApi and gRPC pipelines without requiring an external altcha service. Validated 4 scenarios on each transport (8 total, all pass): HTTP /api/command/protectedAction POST 6001 gRPC :6000 ------------------------------------------------------------------- no AltchaSolution 401 Unauthenticated AltchaSolution = "wrong" 401 Unauthenticated AltchaSolution = "valid-solution" 200 result OK + result addUser (no [Altcha]) 200 result OK + result The last row confirms backward compatibility: a request type that isn't decorated with [Altcha] bypasses the check entirely — the AltchaAuthorizationCheck self-applies and no-ops, and any consumer that doesn't call AddSvrntyAltcha() sees zero behavior change. Generated CommandServiceImpl.g.cs verified to include the ICommandAuthorizationCheck loop after validation, before handler invocation, with the materialized command instance in ctx.Command. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
891894d136
commit
ede9548cba
@ -1,6 +1,8 @@
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using Svrnty.CQRS;
|
||||
using Svrnty.CQRS.Abstractions;
|
||||
using Svrnty.CQRS.Altcha;
|
||||
using Svrnty.CQRS.Altcha.Abstractions;
|
||||
using Svrnty.CQRS.DynamicQuery;
|
||||
using Svrnty.CQRS.FluentValidation;
|
||||
using Svrnty.CQRS.Grpc;
|
||||
@ -27,8 +29,14 @@ builder.Services.AddDynamicQueryWithProvider<User, UserQueryableProvider>();
|
||||
// Register commands and queries with validators
|
||||
builder.Services.AddCommand<AddUserCommand, int, AddUserCommandHandler, AddUserCommandValidator>();
|
||||
builder.Services.AddCommand<RemoveUserCommand, RemoveUserCommandHandler>();
|
||||
builder.Services.AddCommand<ProtectedActionCommand, string, ProtectedActionCommandHandler>();
|
||||
builder.Services.AddQuery<FetchUserQuery, User, FetchUserQueryHandler>();
|
||||
|
||||
// Wire the Altcha check + a stub verifier so [Altcha] commands run
|
||||
// through the ICommandAuthorizationCheck pipeline.
|
||||
builder.Services.AddSvrntyAltcha();
|
||||
builder.Services.AddSingleton<IAltchaVerifier, StubAltchaVerifier>();
|
||||
|
||||
// Configure CQRS with fluent API
|
||||
builder.Services.AddSvrntyCqrs(cqrs =>
|
||||
{
|
||||
|
||||
37
Svrnty.Sample/ProtectedActionCommand.cs
Normal file
37
Svrnty.Sample/ProtectedActionCommand.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using Svrnty.CQRS.Abstractions;
|
||||
using Svrnty.CQRS.Altcha.Abstractions;
|
||||
|
||||
namespace Svrnty.Sample;
|
||||
|
||||
// Exercises the ICommandAuthorizationCheck seam at runtime.
|
||||
// The command is decorated with [Altcha] and carries the solution
|
||||
// through IHasAltchaSolution. With Svrnty.CQRS.Altcha + a registered
|
||||
// IAltchaVerifier, the framework's check pipeline reads the field,
|
||||
// calls the verifier, and short-circuits on failure.
|
||||
[Altcha]
|
||||
public sealed class ProtectedActionCommand : IHasAltchaSolution
|
||||
{
|
||||
public string Action { get; set; } = string.Empty;
|
||||
public string? AltchaSolution { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ProtectedActionCommandHandler : ICommandHandler<ProtectedActionCommand, string>
|
||||
{
|
||||
public Task<string> HandleAsync(ProtectedActionCommand command, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult($"executed:{command.Action}");
|
||||
}
|
||||
}
|
||||
|
||||
// Stub verifier that doesn't talk to an external altcha service —
|
||||
// enough to exercise the check pipeline in isolation. Treats the
|
||||
// literal string "valid-solution" as a passing PoW solution.
|
||||
public sealed class StubAltchaVerifier : IAltchaVerifier
|
||||
{
|
||||
public Task<AltchaVerifyResult> VerifyAsync(string payload, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (payload == "valid-solution")
|
||||
return Task.FromResult(AltchaVerifyResult.Success);
|
||||
return Task.FromResult(AltchaVerifyResult.Fail("stub-rejected"));
|
||||
}
|
||||
}
|
||||
@ -9,6 +9,9 @@ service CommandService {
|
||||
// AddUserCommand operation
|
||||
rpc AddUser (AddUserCommandRequest) returns (AddUserCommandResponse);
|
||||
|
||||
// ProtectedActionCommand operation
|
||||
rpc ProtectedAction (ProtectedActionCommandRequest) returns (ProtectedActionCommandResponse);
|
||||
|
||||
// RemoveUserCommand operation
|
||||
rpc RemoveUser (RemoveUserCommandRequest) returns (RemoveUserCommandResponse);
|
||||
|
||||
@ -40,6 +43,17 @@ message AddUserCommandResponse {
|
||||
int32 result = 1;
|
||||
}
|
||||
|
||||
// Request message for ProtectedActionCommand
|
||||
message ProtectedActionCommandRequest {
|
||||
string action = 1;
|
||||
string altcha_solution = 2;
|
||||
}
|
||||
|
||||
// Response message for ProtectedActionCommand
|
||||
message ProtectedActionCommandResponse {
|
||||
string result = 1;
|
||||
}
|
||||
|
||||
// Request message for RemoveUserCommand
|
||||
message RemoveUserCommandRequest {
|
||||
int32 user_id = 1;
|
||||
|
||||
@ -33,6 +33,8 @@
|
||||
<ProjectReference Include="..\Svrnty.CQRS.DynamicQuery\Svrnty.CQRS.DynamicQuery.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.DynamicQuery.MinimalApi\Svrnty.CQRS.DynamicQuery.MinimalApi.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.Grpc.Abstractions\Svrnty.CQRS.Grpc.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.Altcha.Abstractions\Svrnty.CQRS.Altcha.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.Altcha\Svrnty.CQRS.Altcha.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Import the proto generation targets for testing (in production this would come from the NuGet package) -->
|
||||
|
||||
Loading…
Reference in New Issue
Block a user