feat(audit): ScadaBridge IAuditActorAccessor + wire audit Actor from Auth principal at authenticated emit sites (Phase 3)
This commit is contained in:
@@ -33,14 +33,18 @@ namespace ZB.MOM.WW.ScadaBridge.InboundAPI.Middleware;
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Actor resolution.</b> Inbound API auth runs inside the endpoint handler
|
||||
/// <b>Actor resolution.</b> The API-key auth path runs inside the endpoint handler
|
||||
/// (no <c>UseAuthentication</c>-backed scheme populates <see cref="HttpContext.User"/>
|
||||
/// for X-API-Key callers), so the handler stashes the resolved API key name on
|
||||
/// for Bearer API-key callers), so the handler stashes the resolved API key name on
|
||||
/// <see cref="HttpContext.Items"/> under <see cref="AuditActorItemKey"/> after
|
||||
/// <c>IApiKeyVerifier.VerifyAsync</c> succeeds. The middleware reads it in
|
||||
/// its <c>finally</c> block — on auth failures the key remains absent and
|
||||
/// <see cref="AuditEvent.Actor"/> stays null (we never echo back an
|
||||
/// unauthenticated principal).
|
||||
/// its <c>finally</c> block. Phase 3: when no API-key name is stashed, the actor is
|
||||
/// sourced from the authenticated <em>interactive</em> principal via
|
||||
/// <see cref="IAuditActorAccessor"/> (a cookie/LDAP-authenticated inbound user,
|
||||
/// keyed off the canonical username claim). On auth failures (401/403) the actor is
|
||||
/// forced null before resolution runs, and the accessor itself returns null for an
|
||||
/// unauthenticated principal — so <see cref="AuditEvent.Actor"/> stays null and we
|
||||
/// never echo back an unauthenticated principal.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
@@ -90,6 +94,7 @@ public sealed class AuditWriteMiddleware
|
||||
private readonly ICentralAuditWriter _auditWriter;
|
||||
private readonly ILogger<AuditWriteMiddleware> _logger;
|
||||
private readonly IOptionsMonitor<AuditLogOptions> _options;
|
||||
private readonly IAuditActorAccessor? _actorAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the middleware with its required dependencies.
|
||||
@@ -98,16 +103,25 @@ public sealed class AuditWriteMiddleware
|
||||
/// <param name="auditWriter">Central audit writer used to persist inbound API audit events.</param>
|
||||
/// <param name="logger">Logger for this middleware.</param>
|
||||
/// <param name="options">Live-reloadable audit log options, read per-request.</param>
|
||||
/// <param name="actorAccessor">
|
||||
/// Phase 3 (optional): resolves the audit <see cref="AuditEvent.Actor"/> from the
|
||||
/// authenticated principal on a cookie/LDAP-authenticated inbound request. Optional
|
||||
/// so existing tests (and any composition without the accessor registered) still
|
||||
/// construct the middleware; when absent, actor resolution falls back to the
|
||||
/// stashed API-key name only.
|
||||
/// </param>
|
||||
public AuditWriteMiddleware(
|
||||
RequestDelegate next,
|
||||
ICentralAuditWriter auditWriter,
|
||||
ILogger<AuditWriteMiddleware> logger,
|
||||
IOptionsMonitor<AuditLogOptions> options)
|
||||
IOptionsMonitor<AuditLogOptions> options,
|
||||
IAuditActorAccessor? actorAccessor = null)
|
||||
{
|
||||
_next = next ?? throw new ArgumentNullException(nameof(next));
|
||||
_auditWriter = auditWriter ?? throw new ArgumentNullException(nameof(auditWriter));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_actorAccessor = actorAccessor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -472,13 +486,24 @@ public sealed class AuditWriteMiddleware
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the API key name the endpoint handler stashed on
|
||||
/// <see cref="HttpContext.Items"/> after successful auth. Falls back to
|
||||
/// the authenticated user name when an ASP.NET scheme has populated
|
||||
/// <see cref="HttpContext.User"/> (defensive — currently unused for inbound
|
||||
/// API but cheap and forward-compatible).
|
||||
/// Resolves the audit <see cref="AuditEvent.Actor"/> for a non-auth-failure
|
||||
/// inbound request:
|
||||
/// <list type="number">
|
||||
/// <item><description>the API key name the endpoint handler stashed on
|
||||
/// <see cref="HttpContext.Items"/> after successful key auth (the
|
||||
/// key-authenticated path — the canonical identity of an API-key caller);</description></item>
|
||||
/// <item><description>otherwise the authenticated <em>interactive</em> principal
|
||||
/// resolved through <see cref="IAuditActorAccessor"/> (Phase 3 — a
|
||||
/// cookie/LDAP-authenticated inbound user, sourced from the canonical username
|
||||
/// claim). The accessor reads the ambient <see cref="HttpContext.User"/>, so the
|
||||
/// fall-through here only fires when no API-key name was stashed;</description></item>
|
||||
/// <item><description>otherwise <c>null</c> — never echo an unauthenticated
|
||||
/// principal back as an actor.</description></item>
|
||||
/// </list>
|
||||
/// The accessor is optional (constructor default <c>null</c>); when absent only
|
||||
/// the stashed API-key name is consulted, preserving the pre-Phase-3 behaviour.
|
||||
/// </summary>
|
||||
private static string? ResolveActor(HttpContext ctx)
|
||||
private string? ResolveActor(HttpContext ctx)
|
||||
{
|
||||
if (ctx.Items.TryGetValue(AuditActorItemKey, out var stashed)
|
||||
&& stashed is string name
|
||||
@@ -487,13 +512,11 @@ public sealed class AuditWriteMiddleware
|
||||
return name;
|
||||
}
|
||||
|
||||
var user = ctx.User;
|
||||
if (user?.Identity is { IsAuthenticated: true, Name: { Length: > 0 } userName })
|
||||
{
|
||||
return userName;
|
||||
}
|
||||
|
||||
return null;
|
||||
// Phase 3: an interactive cookie/LDAP-authenticated inbound user records
|
||||
// their real identity as Actor. Returns null for the key-authenticated
|
||||
// and auth-failure paths (no authenticated interactive principal), so the
|
||||
// existing API-key/auth-failure behaviour is preserved.
|
||||
return _actorAccessor?.CurrentActor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user