feat(audit): persist OPC UA certificate-trust decisions to the central audit log (ScadaBridge#43)
New AuditChannel.Security with AuditKind.CertTrust / CertTrustRemove (appended, ordinals stable). Both central entry points - the management actor handlers and the Central UI CertManagementService - write a best-effort ICentralAuditWriter row after the site outcome is known (Delivered when all reachable site nodes acked, Failed otherwise, incl. relay timeouts), naming actor, site, thumbprint and connection. Shared builder: CertTrustAuditEvents. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
@@ -4095,7 +4095,8 @@ public class ManagementActor : ReceiveActor
|
||||
?? throw new ManagementCommandException("SiteIdentifier is required when trusting a server certificate via the management API.");
|
||||
await EnforceSiteScopeForIdentifier(sp, user, site);
|
||||
var commService = sp.GetRequiredService<CommunicationService>();
|
||||
return await commService.TrustServerCertAsync(site, cmd);
|
||||
return await RelayCertTrustAsync(sp, user, remove: false, site, cmd.Thumbprint, cmd.ConnectionName,
|
||||
() => commService.TrustServerCertAsync(site, cmd));
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleRemoveServerCert(IServiceProvider sp, RemoveServerCertCommand cmd, AuthenticatedUser user)
|
||||
@@ -4104,7 +4105,50 @@ public class ManagementActor : ReceiveActor
|
||||
?? throw new ManagementCommandException("SiteIdentifier is required when removing a server certificate via the management API.");
|
||||
await EnforceSiteScopeForIdentifier(sp, user, site);
|
||||
var commService = sp.GetRequiredService<CommunicationService>();
|
||||
return await commService.RemoveServerCertAsync(site, cmd);
|
||||
return await RelayCertTrustAsync(sp, user, remove: true, site, cmd.Thumbprint, connectionName: null,
|
||||
() => commService.RemoveServerCertAsync(site, cmd));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a cert trust/remove relay and records the decision in the central AuditLog
|
||||
/// (Gitea #43). The row is written AFTER the outcome is known, with the real status; a relay
|
||||
/// exception is audited as Failed and rethrown unchanged. Audit is best-effort and never alters
|
||||
/// the relay's result.
|
||||
/// </summary>
|
||||
private static async Task<CertTrustResult> RelayCertTrustAsync(
|
||||
IServiceProvider sp, AuthenticatedUser user, bool remove, string site, string thumbprint,
|
||||
string? connectionName, Func<Task<CertTrustResult>> relay)
|
||||
{
|
||||
CertTrustResult result;
|
||||
try
|
||||
{
|
||||
result = await relay();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await WriteCertTrustAuditAsync(sp, user, remove, site, thumbprint, connectionName, null, ex.Message);
|
||||
throw;
|
||||
}
|
||||
await WriteCertTrustAuditAsync(sp, user, remove, site, thumbprint, connectionName, result, null);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static async Task WriteCertTrustAuditAsync(
|
||||
IServiceProvider sp, AuthenticatedUser user, bool remove, string site, string thumbprint,
|
||||
string? connectionName, CertTrustResult? result, string? exceptionMessage)
|
||||
{
|
||||
try
|
||||
{
|
||||
var evt = ZB.MOM.WW.ScadaBridge.Commons.Types.Audit.CertTrustAuditEvents.Create(
|
||||
remove, user.Username, site, thumbprint, connectionName, result, exceptionMessage);
|
||||
await sp.GetRequiredService<ICentralAuditWriter>().WriteAsync(evt);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
sp.GetService<ILogger<ManagementActor>>()?.LogWarning(
|
||||
ex, "Best-effort cert-trust audit emission failed (site={Site}, thumbprint={Thumbprint}); the trust decision itself is unaffected.",
|
||||
site, thumbprint);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleListServerCerts(IServiceProvider sp, ListServerCertsCommand cmd, AuthenticatedUser user)
|
||||
|
||||
Reference in New Issue
Block a user