fix(archreview-p1): SEC-02/12/20 dashboard + observability hardening

- SEC-02: DashboardAuthorizationHandler restricts the loopback and
  Authentication:Mode=Disabled bypasses to read-only. They now satisfy only a
  Viewer-bearing requirement (AnyDashboardRole), never AdminOnly, so anonymous
  localhost can view the dashboard but cannot reach API-key CRUD or session
  Close/Kill at the policy layer (previously guarded only by service re-checks).
- SEC-12: DashboardSessionAdminService emits canonical AuditEvents through
  IAuditWriter (actions dashboard-close-session / dashboard-kill-worker, category
  SessionAdmin) on Success/Failure/Denied, mirroring the API-key audit path so
  destructive session actions leave durable, queryable rows.
- SEC-20: drop the unbounded session_id tag from the exported
  mxgateway.heartbeats.failed counter (per-session detail stays in the snapshot/log).

Docs updated same-change: CLAUDE.md (read-only loopback + 5-min bearer),
GatewayDashboardDesign.md (bypass scoping + session-admin audit), Metrics.md.
Server build clean; 30/30 targeted + 295/295 Dashboard/Security/App/Metrics sweep.
This commit is contained in:
Joseph Doherty
2026-07-09 07:47:51 -04:00
parent c2df4a0aae
commit 74e815c4d7
10 changed files with 347 additions and 20 deletions
@@ -11,6 +11,18 @@ namespace ZB.MOM.WW.MxGateway.Server.Dashboard;
/// (b) authentication is fully disabled, or (c) the request is from loopback
/// and <c>MxGateway:Dashboard:AllowAnonymousLocalhost</c> is on.
/// </summary>
/// <remarks>
/// The environment bypasses in (b) and (c) grant <em>read-only</em> access only: they
/// satisfy a requirement that includes <see cref="DashboardRoles.Viewer"/> (i.e.
/// <see cref="DashboardAuthorizationRequirement.AnyDashboardRole"/>) but never the
/// <see cref="DashboardAuthorizationRequirement.AdminOnly"/> requirement. Destructive/admin
/// surfaces still require a real Admin role claim, preserving the documented
/// "anonymous localhost is read-only" contract at the policy layer rather than by accident
/// in downstream service re-checks. The loopback test trusts
/// <c>Connection.RemoteIpAddress</c>; if forwarded-headers middleware is ever added upstream,
/// <see cref="IsLoopbackRequest"/> must be revisited so a spoofed <c>X-Forwarded-For</c>
/// cannot masquerade as loopback.
/// </remarks>
public sealed class DashboardAuthorizationHandler(
IHttpContextAccessor httpContextAccessor,
IOptions<GatewayOptions> options) : AuthorizationHandler<DashboardAuthorizationRequirement>
@@ -22,14 +34,18 @@ public sealed class DashboardAuthorizationHandler(
{
GatewayOptions gatewayOptions = options.Value;
if (gatewayOptions.Authentication.Mode == AuthenticationMode.Disabled)
// Environment bypasses are read-only: they satisfy the Viewer requirement
// (AnyDashboardRole) but never AdminOnly, which lacks Viewer in RequiredRoles.
bool grantsReadOnly = requirement.RequiredRoles.Contains(DashboardRoles.Viewer);
if (grantsReadOnly && gatewayOptions.Authentication.Mode == AuthenticationMode.Disabled)
{
context.Succeed(requirement);
return Task.CompletedTask;
}
if (gatewayOptions.Dashboard.AllowAnonymousLocalhost && IsLoopbackRequest())
if (grantsReadOnly && gatewayOptions.Dashboard.AllowAnonymousLocalhost && IsLoopbackRequest())
{
context.Succeed(requirement);