feat(cli): working audit verify-chain via the VerifyAuditChain management command (ScadaBridge#41)
verify-chain was a no-op stub that printed "not enabled" and exited 0 without contacting the server. It now sends VerifyAuditChain(Month) over POST /management and maps the verdict to the exit code: 0 intact or not chained, 3 broken (first break: partition, ChainSeq, EventId, kind), 1 error, 2 unauthorized. Table and JSON renderings. ManagementActor: new HandleVerifyAuditChain arm, gated on the OperationalAudit roles (Administrator, Viewer) and refusing a site-scoped caller because a month's chain spans every site; INVALID_ARGUMENT for a bad month, CHAIN_BUSY when the chain lock is held; long-running Ask timeout. Result enums serialise as names. Parity: RequiredRoleMatrixTests freezes the gate; ManagementSurfaceParityTests now probes `audit verify-chain` (the one audit leaf that is a management contract) instead of skipping the whole audit group. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
@@ -246,6 +246,9 @@ public class ManagementActor : ReceiveActor
|
||||
// three Live run commands stay DeployerOnly.
|
||||
private static readonly string[] FlowDebuggerReplayRoles = AuthorizationPolicies.FlowDebuggerAccessRoles;
|
||||
|
||||
// ScadaBridge#41: verify-chain is an audit read — the OperationalAudit role set.
|
||||
private static readonly string[] OperationalAuditReaders = AuthorizationPolicies.OperationalAuditRoles;
|
||||
|
||||
// arch-review C6: area management is reconciled to any-of [Designer, Deployer].
|
||||
// Areas are authored on both real surfaces — the Designer template/topology
|
||||
// tooling AND the Central UI Deployment Topology workflow (Topology.razor is
|
||||
@@ -384,6 +387,12 @@ public class ManagementActor : ReceiveActor
|
||||
// (arch-review UA1).
|
||||
ListSecuredWritesCommand => SecuredWriteReaders,
|
||||
|
||||
// AuditLog hash-chain verification (ScadaBridge#41): an audit read, so the
|
||||
// OperationalAudit roles (alog.md §15.1 — verify-chain carries audit query's
|
||||
// permission). A site-scoped caller is refused in the handler: a month's chain
|
||||
// spans every site.
|
||||
VerifyAuditChainCommand => OperationalAuditReaders,
|
||||
|
||||
// Read-only queries -- any authenticated user
|
||||
_ => null
|
||||
};
|
||||
@@ -554,6 +563,7 @@ public class ManagementActor : ReceiveActor
|
||||
|
||||
// Audit Log
|
||||
QueryAuditLogCommand cmd => await HandleQueryAuditLog(sp, cmd),
|
||||
VerifyAuditChainCommand cmd => await HandleVerifyAuditChain(sp, cmd, user),
|
||||
|
||||
// Health
|
||||
GetHealthSummaryCommand => HandleGetHealthSummary(sp),
|
||||
@@ -3210,6 +3220,42 @@ public class ManagementActor : ReceiveActor
|
||||
return new AuditLogQueryResult(entries, totalCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies one month of the AuditLog hash chain (ScadaBridge#41). The month must be
|
||||
/// <c>YYYY-MM</c>; a site-scoped, non-Administrator caller is refused because the chain
|
||||
/// covers every site's rows and a break report names rows by EventId.
|
||||
/// </summary>
|
||||
private static async Task<object?> HandleVerifyAuditChain(
|
||||
IServiceProvider sp, VerifyAuditChainCommand cmd, AuthenticatedUser user)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(cmd.Month)
|
||||
|| !DateTime.TryParseExact(cmd.Month, "yyyy-MM", System.Globalization.CultureInfo.InvariantCulture,
|
||||
System.Globalization.DateTimeStyles.None, out var month))
|
||||
{
|
||||
throw new ManagementCommandException(
|
||||
$"Invalid month '{cmd.Month}'. Expected YYYY-MM (e.g. 2026-05).", "INVALID_ARGUMENT");
|
||||
}
|
||||
|
||||
if (user.PermittedSiteIds.Length > 0
|
||||
&& !user.Roles.Contains(Roles.Administrator, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new SiteScopeViolationException(
|
||||
"Access denied: the audit hash chain spans every site; verify-chain needs an "
|
||||
+ "OperationalAudit role that is not scoped to specific sites.");
|
||||
}
|
||||
|
||||
var chain = sp.GetRequiredService<IAuditLogHashChain>();
|
||||
try
|
||||
{
|
||||
return await chain.VerifyMonthAsync(
|
||||
new DateTime(month.Year, month.Month, 1, 0, 0, 0, DateTimeKind.Utc));
|
||||
}
|
||||
catch (TimeoutException ex)
|
||||
{
|
||||
throw new ManagementCommandException(ex.Message, "CHAIN_BUSY");
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Health handlers
|
||||
// ========================================================================
|
||||
|
||||
Reference in New Issue
Block a user