From f1534920deb64e6fb7c82c29a85841282d024641 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 16 Jul 2026 18:19:24 -0400 Subject: [PATCH] test(secrets): G-2c guard AdminUI DriverConfig secret: ref round-trip (Task 9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verify + regression-guard that the AdminUI driver-config editor persists secret:/ env:/file: refs verbatim and never resolve-then-resaves cleartext (which would re-leak the secret into the config store, defeating G-2). Confirmed structurally safe: DriverConfig is persisted as an opaque JSON string via RawTreeService, which has NO secret-resolver dependency (only the DbContext factory) — resolution is impossible on the save path. Resolution stays confined to driver-instantiation / Test-connect (Tasks 7-8); GalaxyDriverBrowser resolves into a connect-scoped local only, never writing back. Three round-trip tests (OpcUaClient Password/UserCertPassword, Galaxy ApiKeySecretRef, env:/file: forms) exercise the real RawTreeService seam and assert byte-identical save→reload. No product change (no leak found). 662 AdminUI tests pass. --- .../RawTreeServiceSecretRefRoundTripTests.cs | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/RawTreeServiceSecretRefRoundTripTests.cs diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/RawTreeServiceSecretRefRoundTripTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/RawTreeServiceSecretRefRoundTripTests.cs new file mode 100644 index 00000000..d47c219e --- /dev/null +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/RawTreeServiceSecretRefRoundTripTests.cs @@ -0,0 +1,110 @@ +using Microsoft.EntityFrameworkCore; +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.AdminUI.Uns; +using ZB.MOM.WW.OtOpcUa.Configuration; + +namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns; + +/// +/// Secrets-adoption (G-2c) regression guard: the AdminUI driver-config editor persists DriverConfig +/// as an OPAQUE JSON string through (see UpdateDriverAsync / +/// CreateDriverAsyncentity.DriverConfig = driverConfigJson verbatim, and +/// LoadDriverForEditAsync reads it back verbatim). A secret:/env:/file: +/// reference stored in a driver secret field (OpcUaClient Password/UserCertificatePassword, +/// Galaxy Gateway.ApiKeySecretRef) MUST survive save→reload as the literal ref string — the save +/// path must never resolve-then-resave cleartext (which would re-leak the secret into the config store and +/// defeat G-2). Secret resolution belongs ONLY at driver-instantiation / Test-connect time (Tasks 7-8), +/// never on the AdminUI persistence path — and indeed takes no secret resolver +/// (its only dependency is the DbContext factory), so resolution is structurally impossible here. +/// +/// Reuses the shared InMemory fixture, matching the WP3 driver-config tests. +/// +/// +[Trait("Category", "Unit")] +public sealed class RawTreeServiceSecretRefRoundTripTests +{ + private static (RawTreeService Service, string DbName) Seeded() + { + var name = RawTreeTestDb.SeedFresh(); + return (new RawTreeService(RawTreeTestDb.Factory(name)), name); + } + + private static byte[] RowVersionOf(string dbName, string driverId) + { + using var db = RawTreeTestDb.CreateNamed(dbName); + return db.DriverInstances.Single(d => d.DriverInstanceId == driverId).RowVersion; + } + + [Fact] + public async Task UpdateDriver_preserves_OpcUaClient_secret_refs_verbatim_on_save_and_reload() + { + var (service, dbName) = Seeded(); + var rv = RowVersionOf(dbName, RawTreeTestDb.FolderedDriverId); + + // The editor emits the raw ref strings the user typed — they must NOT be resolved on save. + const string config = + "{\"Password\":\"secret:opcua/x/pw\",\"UserCertificatePassword\":\"secret:opcua/x/certpw\"}"; + + var result = await service.UpdateDriverAsync( + RawTreeTestDb.FolderedDriverId, "modbus-1", config, null, rv); + result.Ok.ShouldBeTrue(); + + // Reload through the same seam the modal uses. + var reloaded = await service.LoadDriverForEditAsync(RawTreeTestDb.FolderedDriverId); + reloaded.ShouldNotBeNull(); + + // Byte-identical: nothing on the save path rewrote the string. + reloaded!.DriverConfig.ShouldBe(config); + // The literal refs survived (not resolved to cleartext, not stripped). + reloaded.DriverConfig.ShouldContain("secret:opcua/x/pw"); + reloaded.DriverConfig.ShouldContain("secret:opcua/x/certpw"); + + // And the raw column agrees with the projection. + using var db = RawTreeTestDb.CreateNamed(dbName); + db.DriverInstances.Single(d => d.DriverInstanceId == RawTreeTestDb.FolderedDriverId) + .DriverConfig.ShouldBe(config); + } + + [Fact] + public async Task CreateDriver_preserves_Galaxy_ApiKeySecretRef_verbatim_on_save_and_reload() + { + var (service, dbName) = Seeded(); + + const string config = "{\"Gateway\":{\"ApiKeySecretRef\":\"secret:galaxy/x/apikey\"}}"; + + var created = await service.CreateDriverAsync( + RawTreeTestDb.ClusterId, RawTreeTestDb.RootFolderId, "galaxy-1", "Galaxy", config); + created.Ok.ShouldBeTrue(); + created.CreatedId.ShouldNotBeNull(); + + var reloaded = await service.LoadDriverForEditAsync(created.CreatedId!); + reloaded.ShouldNotBeNull(); + + reloaded!.DriverConfig.ShouldBe(config); + reloaded.DriverConfig.ShouldContain("secret:galaxy/x/apikey"); + + using var db = RawTreeTestDb.CreateNamed(dbName); + db.DriverInstances.Single(d => d.DriverInstanceId == created.CreatedId) + .DriverConfig.ShouldBe(config); + } + + [Fact] + public async Task UpdateDriver_preserves_env_and_file_ref_forms_verbatim() + { + var (service, dbName) = Seeded(); + var rv = RowVersionOf(dbName, RawTreeTestDb.FolderedDriverId); + + const string config = + "{\"Password\":\"env:OPCUA_PW\",\"UserCertificatePassword\":\"file:/run/secrets/certpw\"}"; + + var result = await service.UpdateDriverAsync( + RawTreeTestDb.FolderedDriverId, "modbus-1", config, null, rv); + result.Ok.ShouldBeTrue(); + + var reloaded = await service.LoadDriverForEditAsync(RawTreeTestDb.FolderedDriverId); + reloaded!.DriverConfig.ShouldBe(config); + reloaded.DriverConfig.ShouldContain("env:OPCUA_PW"); + reloaded.DriverConfig.ShouldContain("file:/run/secrets/certpw"); + } +}