f1534920de
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.
111 lines
4.9 KiB
C#
111 lines
4.9 KiB
C#
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");
|
|
}
|
|
}
|