feat(audit): OtOpcUa IAuditActorAccessor seam + HTTP impl (audit Actor from Auth principal) (Phase 3)
v2-ci / build (push) Failing after 40s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
v2-ci / build (push) Failing after 40s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
Introduces the IAuditActorAccessor seam and HttpAuditActorAccessor impl so the ZB.MOM.WW.Audit.AuditEvent Actor field can be sourced from the authenticated Blazor cookie principal (ZbClaimTypes.Username) when structured emitters are added. Adds the AuditActor.Resolve static helper (accessor value → SystemFallback/"system") as the canonical pattern for future emit sites. Wires DI in AddOtOpcUaAuth (TryAddScoped) with AddHttpContextAccessor(). The structured AuditEvent path remains DORMANT — no live emit sites exist; seam is forward-looking. SP-based audit path left untouched. 9 new unit tests all green; Security (54) and ControlPlane (45) test suites fully pass.
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Security.Audit;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Security.Tests.Audit;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="AuditActor"/> — the static resolution helper that sources the
|
||||
/// <c>Actor</c> field of a canonical <c>ZB.MOM.WW.Audit.AuditEvent</c> from the current
|
||||
/// HTTP principal and falls back to a configurable value when no principal is available.
|
||||
/// </summary>
|
||||
public sealed class AuditActorTests
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="AuditActor.Resolve(IAuditActorAccessor?)"/> returns the accessor's value
|
||||
/// when the accessor returns a non-null string.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Resolve_returns_accessor_value_when_present()
|
||||
{
|
||||
var accessor = new StubAccessor("alice");
|
||||
|
||||
AuditActor.Resolve(accessor).ShouldBe("alice");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="AuditActor.Resolve(IAuditActorAccessor?)"/> returns
|
||||
/// <see cref="AuditActor.SystemFallback"/> when the accessor returns null
|
||||
/// (unauthenticated / no HTTP context).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Resolve_returns_system_fallback_when_accessor_returns_null()
|
||||
{
|
||||
var accessor = new StubAccessor(null);
|
||||
|
||||
AuditActor.Resolve(accessor).ShouldBe(AuditActor.SystemFallback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="AuditActor.Resolve(IAuditActorAccessor?)"/> returns
|
||||
/// <see cref="AuditActor.SystemFallback"/> when the accessor reference itself is null
|
||||
/// (e.g. in a background/non-HTTP context where DI did not inject the accessor).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Resolve_returns_system_fallback_when_accessor_is_null()
|
||||
{
|
||||
AuditActor.Resolve(null).ShouldBe(AuditActor.SystemFallback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="AuditActor.Resolve(IAuditActorAccessor?,string)"/> uses the explicit
|
||||
/// fallback string rather than <see cref="AuditActor.SystemFallback"/> when the accessor
|
||||
/// returns null.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Resolve_uses_explicit_fallback_when_accessor_returns_null()
|
||||
{
|
||||
var accessor = new StubAccessor(null);
|
||||
|
||||
AuditActor.Resolve(accessor, "scheduler").ShouldBe("scheduler");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="AuditActor.Resolve(IAuditActorAccessor?,string)"/> prefers the accessor's
|
||||
/// value over the explicit fallback when the accessor returns a non-null string.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Resolve_prefers_accessor_value_over_explicit_fallback()
|
||||
{
|
||||
var accessor = new StubAccessor("bob");
|
||||
|
||||
AuditActor.Resolve(accessor, "scheduler").ShouldBe("bob");
|
||||
}
|
||||
|
||||
// ── stub ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
private sealed class StubAccessor(string? value) : IAuditActorAccessor
|
||||
{
|
||||
public string? CurrentActor { get; } = value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.Auth.AspNetCore;
|
||||
using ZB.MOM.WW.OtOpcUa.Security.Audit;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Security.Tests.Audit;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="HttpAuditActorAccessor"/>.
|
||||
/// <para>
|
||||
/// Covers the three cases:
|
||||
/// <list type="bullet">
|
||||
/// <item>Authenticated principal with a <see cref="ZbClaimTypes.Username"/> claim →
|
||||
/// returns the username claim value.</item>
|
||||
/// <item>Authenticated principal with only a <see cref="ZbClaimTypes.Name"/> / no
|
||||
/// username claim → falls back to the Name claim.</item>
|
||||
/// <item>No HTTP context (null) or unauthenticated principal → returns
|
||||
/// <see langword="null"/>.</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class HttpAuditActorAccessorTests
|
||||
{
|
||||
// ── helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
private static IHttpContextAccessor ContextWith(ClaimsPrincipal principal)
|
||||
{
|
||||
var context = new DefaultHttpContext { User = principal };
|
||||
return new HttpContextAccessorStub(context);
|
||||
}
|
||||
|
||||
private static IHttpContextAccessor NoContext() =>
|
||||
new HttpContextAccessorStub(null);
|
||||
|
||||
private static ClaimsPrincipal AuthenticatedWith(params Claim[] claims)
|
||||
{
|
||||
var identity = new ClaimsIdentity(
|
||||
claims,
|
||||
authenticationType: "TestScheme", // non-null authenticationType → IsAuthenticated = true
|
||||
nameType: ZbClaimTypes.Name,
|
||||
roleType: ZbClaimTypes.Role);
|
||||
return new ClaimsPrincipal(identity);
|
||||
}
|
||||
|
||||
private static ClaimsPrincipal Unauthenticated() =>
|
||||
new(new ClaimsIdentity()); // no authenticationType → IsAuthenticated = false
|
||||
|
||||
// ── tests ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// An authenticated principal that carries <see cref="ZbClaimTypes.Username"/>
|
||||
/// returns exactly that claim value — it is the canonical actor string.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Returns_username_claim_for_authenticated_principal()
|
||||
{
|
||||
var principal = AuthenticatedWith(
|
||||
new Claim(ZbClaimTypes.Username, "alice"),
|
||||
new Claim(ZbClaimTypes.Name, "alice-name"),
|
||||
new Claim(ZbClaimTypes.DisplayName, "Alice User"));
|
||||
|
||||
var sut = new HttpAuditActorAccessor(ContextWith(principal));
|
||||
|
||||
sut.CurrentActor.ShouldBe("alice");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When the principal has no <see cref="ZbClaimTypes.Username"/> claim but does have
|
||||
/// a <see cref="ZbClaimTypes.Name"/> claim, the Name claim value is returned as the
|
||||
/// fallback actor.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Falls_back_to_Name_claim_when_Username_claim_is_absent()
|
||||
{
|
||||
var principal = AuthenticatedWith(
|
||||
new Claim(ZbClaimTypes.Name, "bob"));
|
||||
|
||||
var sut = new HttpAuditActorAccessor(ContextWith(principal));
|
||||
|
||||
sut.CurrentActor.ShouldBe("bob");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An unauthenticated principal (Identity.IsAuthenticated == false) returns null —
|
||||
/// the caller's fallback (typically <see cref="AuditActor.SystemFallback"/>) is used.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Returns_null_for_unauthenticated_principal()
|
||||
{
|
||||
var sut = new HttpAuditActorAccessor(ContextWith(Unauthenticated()));
|
||||
|
||||
sut.CurrentActor.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When there is no current <c>HttpContext</c> (e.g. background task, actor mailbox
|
||||
/// worker), returns null.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Returns_null_when_no_HttpContext()
|
||||
{
|
||||
var sut = new HttpAuditActorAccessor(NoContext());
|
||||
|
||||
sut.CurrentActor.ShouldBeNull();
|
||||
}
|
||||
|
||||
// ── stub ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
private sealed class HttpContextAccessorStub(HttpContext? context) : IHttpContextAccessor
|
||||
{
|
||||
public HttpContext? HttpContext { get; set; } = context;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user