test(secrets): G-2c guard AdminUI DriverConfig secret: ref round-trip (Task 9)
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.
This commit is contained in:
+110
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Secrets-adoption (G-2c) regression guard: the AdminUI driver-config editor persists <c>DriverConfig</c>
|
||||
/// as an OPAQUE JSON string through <see cref="RawTreeService"/> (see <c>UpdateDriverAsync</c> /
|
||||
/// <c>CreateDriverAsync</c> — <c>entity.DriverConfig = driverConfigJson</c> verbatim, and
|
||||
/// <c>LoadDriverForEditAsync</c> reads it back verbatim). A <c>secret:</c>/<c>env:</c>/<c>file:</c>
|
||||
/// reference stored in a driver secret field (OpcUaClient <c>Password</c>/<c>UserCertificatePassword</c>,
|
||||
/// Galaxy <c>Gateway.ApiKeySecretRef</c>) 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 <see cref="RawTreeService"/> takes no secret resolver
|
||||
/// (its only dependency is the DbContext factory), so resolution is structurally impossible here.
|
||||
/// <para>
|
||||
/// Reuses the shared <see cref="RawTreeTestDb"/> InMemory fixture, matching the WP3 driver-config tests.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user