From fc86e8bfe17a122dbaf4f39c3f3af116bc6b65d3 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 19 Jul 2026 00:57:50 -0400 Subject: [PATCH] fix(secrets): bridge the shared audit seam so /admin/secrets renders (#22) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Secrets.Ui components inject the SHARED ZB.MOM.WW.Audit.IAuditWriter seam, which is deliberately distinct from the repo's own Commons IAuditWriter — the G-4 adoption mounted the UI without registering it, so component activation threw and the page 500'd on every render (hidden behind the login wall; it plausibly never worked). Fix: CentralSharedSeamAuditWriter forwards the shared seam into the central direct-write path (ICentralAuditWriter -> dbo.AuditLog) — NOT the site SQLite chain, which is never resolved on central and has no forwarder there. Registered in the Central composition root next to the Secrets UI authorization wiring. Regression pin: CentralCompositionRootTests resolves the full Secrets.Ui component injection set (ISecretStore / ISecretCipher / shared IAuditWriter) out of the REAL Program.cs via WebApplicationFactory, plus a type check that the seam is the central bridge. Red on the previous commit; the defect class is invisible until the DI graph is actually built. Live-proven on the local docker cluster: /admin/secrets 200 + interactive (Blazor circuit + working @onclick), and secret.add / secret.delete events (both outcomes) landed in central dbo.AuditLog via the bridge. Closes #22 Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts --- src/ZB.MOM.WW.ScadaBridge.Host/Program.cs | 8 ++++ .../Services/CentralSharedSeamAuditWriter.cs | 30 +++++++++++++++ .../CompositionRootTests.cs | 38 +++++++++++++++++++ 3 files changed, 76 insertions(+) create mode 100644 src/ZB.MOM.WW.ScadaBridge.Host/Services/CentralSharedSeamAuditWriter.cs diff --git a/src/ZB.MOM.WW.ScadaBridge.Host/Program.cs b/src/ZB.MOM.WW.ScadaBridge.Host/Program.cs index ddda2787..3ef98a9a 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Host/Program.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Host/Program.cs @@ -192,6 +192,14 @@ try // builds identities with RoleClaimType = ZbClaimTypes.Role, so an Administrator is // authorized for both manage + reveal. Done Host-side to keep Secrets.Ui out of Security. builder.Services.Configure(o => o.AddSecretsAuthorization()); + // Secrets.Ui components audit through the SHARED ZB.MOM.WW.Audit.IAuditWriter seam, + // which is deliberately distinct from the repo's own Commons IAuditWriter — bridge it + // to the central direct-write path (ICentralAuditWriter → dbo.AuditLog), NOT the site + // SQLite chain, which is never resolved on central and has no forwarder here + // (ScadaBridge#22; component activation 500'd without this registration). + builder.Services.AddSingleton( + sp => new ZB.MOM.WW.ScadaBridge.Host.Services.CentralSharedSeamAuditWriter( + sp.GetRequiredService())); builder.Services.AddInboundAPI(); // Inbound-API auth re-arch: the shared ZB.MOM.WW.Auth.ApiKeys verifier + diff --git a/src/ZB.MOM.WW.ScadaBridge.Host/Services/CentralSharedSeamAuditWriter.cs b/src/ZB.MOM.WW.ScadaBridge.Host/Services/CentralSharedSeamAuditWriter.cs new file mode 100644 index 00000000..de687b45 --- /dev/null +++ b/src/ZB.MOM.WW.ScadaBridge.Host/Services/CentralSharedSeamAuditWriter.cs @@ -0,0 +1,30 @@ +using ZB.MOM.WW.Audit; +using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services; + +namespace ZB.MOM.WW.ScadaBridge.Host.Services; + +/// +/// Central-only bridge from the shared-library audit seam +/// () to the central direct-write path +/// ( → dbo.AuditLog). Shared-lib UI components — +/// today the Secrets.Ui components mounted at /admin/secrets — inject the shared +/// seam, which is deliberately distinct from the repo's own +/// (ScadaBridge#22). +/// +/// +/// The target is and NOT the repo's own site +/// writer chain: the site SQLite chain is registered on central but by design +/// never resolved there, and events written to it would dead-end in a local file +/// that no forwarding sidecar drains on a central node. The best-effort contract +/// (audit failures never abort the user-facing action) is inherited from +/// , which swallows and logs — +/// so this forward needs no try/catch of its own. +/// +/// The central direct-write audit path to forward into. +public sealed class CentralSharedSeamAuditWriter(ICentralAuditWriter central) + : ZB.MOM.WW.Audit.IAuditWriter +{ + /// + public Task WriteAsync(AuditEvent auditEvent, CancellationToken cancellationToken = default) + => central.WriteAsync(auditEvent, cancellationToken); +} diff --git a/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/CompositionRootTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/CompositionRootTests.cs index d6ab0f9e..5d89eb9d 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/CompositionRootTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/CompositionRootTests.cs @@ -18,6 +18,7 @@ using ZB.MOM.WW.ScadaBridge.HealthMonitoring; using ZB.MOM.WW.ScadaBridge.Host; using ZB.MOM.WW.ScadaBridge.Host.Actors; using ZB.MOM.WW.ScadaBridge.Host.Health; +using ZB.MOM.WW.ScadaBridge.Host.Services; using ZB.MOM.WW.ScadaBridge.InboundAPI; using ZB.MOM.WW.ScadaBridge.ManagementService; using ZB.MOM.WW.ScadaBridge.NotificationService; @@ -333,6 +334,43 @@ public class CentralCompositionRootTests : IDisposable Assert.NotNull(gate); Assert.IsType(gate); } + + // --- ScadaBridge#22 regression --- + + /// + /// ScadaBridge#22 regression: the Secrets.Ui components mounted at /admin/secrets inject + /// — the SHARED-lib seam, not the repo's own + /// (deliberately distinct per its doc comment). The secrets + /// adoption mounted the UI without bridging that seam, so component activation threw and + /// the page 500'd on every render; the login wall hid it. This resolves the full + /// injection set of every Secrets.Ui component (SecretsList / SecretEditor / + /// RevealButton / ConfirmDeleteModal) out of the REAL central composition root, because + /// the gap is invisible until the graph is actually built — the same defect class as the + /// four caught in the secrets library itself. + /// + [Theory] + [InlineData(typeof(ZB.MOM.WW.Secrets.Abstractions.ISecretStore))] + [InlineData(typeof(ZB.MOM.WW.Secrets.Abstractions.ISecretCipher))] + [InlineData(typeof(ZB.MOM.WW.Audit.IAuditWriter))] + public void Central_Resolves_SecretsUiComponentDependencies(Type serviceType) + { + var service = _factory.Services.GetService(serviceType); + Assert.NotNull(service); + } + + /// + /// The bridge must route secrets-UI audit events into the CENTRAL direct-write path + /// ( → dbo.AuditLog), not the site SQLite chain — the + /// site writer chain is registered on central but by design never resolved there, and + /// events written to it would dead-end in a local file no forwarder ever drains. + /// + [Fact] + public void Central_SharedAuditSeam_IsTheCentralDirectWriteBridge() + { + var writer = _factory.Services.GetService(); + Assert.NotNull(writer); + Assert.IsType(writer); + } } ///