fix(security): validate the API-key pepper at start, finish JWT bearer, circuit-aware audit actor
- ApiKeyOptionsValidator (ValidateOnStart on any driver/admin node): a blank or placeholder Security:ApiKeys:Pepper fails host start instead of rejecting every key after a green boot (R03:2.3). - JWT is finished and opt-in by key (R03:2.4): with Security:Jwt:SigningKey set, a Bearer scheme (JwtBearerAuthenticationHandler over JwtTokenService.TryValidate / BuildValidationParameters, fail closed) accepts the tokens /auth/token mints; the default scheme is a cookie-or-bearer policy scheme and the fallback policy names it, so /api/* and every RequireAuthorization endpoint take a browser session OR a machine token. JwtOptions are validated at start (JwtOptionsValidator). Without a key, /auth/token is not mapped (404, not the former constructor 500) and no bearer scheme exists. A direct AddJwtBearer needs Microsoft.AspNetCore.Authentication.JwtBearer in Directory.Packages.props (not this lane's file) — the handler is a drop-in to swap. - AuthenticationStateAuditActorAccessor is the registered IAuditActorAccessor: the Blazor circuit's principal first, HttpContext as the fallback (R03:2.5). HttpAuditActorAccessor shares its resolver. - CookieAuthenticationStateProvider (unregistered, fire-and-forget ping loop) is deleted (R03:1.5/2.5). - /auth/login logs the LDAP exception at Warning before answering 503 (R03:2.7). - Admin operations reply with a fixed message + correlation id instead of ex.Message (R03:4.1). Refs #733 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
+68
@@ -0,0 +1,68 @@
|
||||
using Akka.Actor;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Admin;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Types;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration;
|
||||
using ZB.MOM.WW.OtOpcUa.ControlPlane.AdminOperations;
|
||||
using ZB.MOM.WW.OtOpcUa.ControlPlane.Tests.Harness;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.ControlPlane.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// R03:4.1 — admin operations echoed <c>ex.Message</c> to the UI, so a SqlException's connection-string
|
||||
/// fragment reached any ConfigEditor browser session. The reply is now a fixed sentence plus the request's
|
||||
/// correlation id; the detail stays in the central log.
|
||||
/// </summary>
|
||||
public sealed class AdminOperationsActorServerFaultMessageTests : ControlPlaneActorTestBase
|
||||
{
|
||||
private const string Secret = "Server=sql-prod-01;Password=hunter2";
|
||||
|
||||
[Fact]
|
||||
public void StartDeployment_server_fault_reply_carries_the_correlation_id_and_not_the_exception_text()
|
||||
{
|
||||
var coordinator = CreateTestProbe("coord");
|
||||
var actor = Sys.ActorOf(AdminOperationsActor.Props(new ThrowingDbFactory(), coordinator.Ref, Enumerable.Empty<IDriverProbe>()));
|
||||
var correlationId = CorrelationId.NewId();
|
||||
|
||||
actor.Tell(new StartDeployment("joe", correlationId));
|
||||
|
||||
var reply = ExpectMsg<StartDeploymentResult>(PresenceBudget);
|
||||
reply.Outcome.ShouldBe(StartDeploymentOutcome.Rejected);
|
||||
reply.Message.ShouldNotBeNull();
|
||||
reply.Message.ShouldNotContain("hunter2");
|
||||
reply.Message.ShouldNotContain("sql-prod-01");
|
||||
reply.Message.ShouldContain(correlationId.ToString()); // the operator can find the log line
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestDriverConnect_probe_throw_reply_carries_the_correlation_id_and_not_the_exception_text()
|
||||
{
|
||||
var coordinator = CreateTestProbe("coord");
|
||||
var actor = Sys.ActorOf(AdminOperationsActor.Props(NewInMemoryDbFactory(), coordinator.Ref, new IDriverProbe[] { new ThrowingProbe() }));
|
||||
var correlationId = Guid.NewGuid();
|
||||
|
||||
actor.Tell(new TestDriverConnect("Throwing", "{}", 5, correlationId));
|
||||
|
||||
var reply = ExpectMsg<TestDriverConnectResult>(PresenceBudget);
|
||||
reply.Ok.ShouldBeFalse();
|
||||
reply.Message.ShouldNotBeNull();
|
||||
reply.Message.ShouldNotContain("hunter2");
|
||||
reply.Message.ShouldContain(correlationId.ToString());
|
||||
}
|
||||
|
||||
private sealed class ThrowingDbFactory : IDbContextFactory<OtOpcUaConfigDbContext>
|
||||
{
|
||||
public OtOpcUaConfigDbContext CreateDbContext() =>
|
||||
throw new InvalidOperationException("Cannot open database: " + Secret);
|
||||
}
|
||||
|
||||
private sealed class ThrowingProbe : IDriverProbe
|
||||
{
|
||||
public string DriverType => "Throwing";
|
||||
public Task<DriverProbeResult> ProbeAsync(string configJson, TimeSpan timeout, CancellationToken ct) =>
|
||||
throw new InvalidOperationException("probe blew up: " + Secret);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user