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); + } } ///