Files
lmxopcua/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Deployments/CredentialReferencePreflightInstanceAndPathTests.cs
T
Joseph Doherty 63331b0416 fix(security): per-instance credential references (#835) and confined OpcUaClient user-certificate path (#836)
#835: inside one driver namespace, a secret:<ns>/<X>/... reference whose first
segment names ANOTHER existing same-type driver instance (id or name,
case-insensitive; another-instance match beats an own-name match, so renaming
one's driver after a victim's unlocks nothing) is refused at save
(DriverConfigSaveGate.ValidateAsync), deploy (CredentialReferenceOtherInstance,
Heidenhain included), Test Connect (TestDriverConnect.DriverInstanceId, ConfigDb
read off the mailbox) and live browse (BrowserSessionService, fail-closed without
a ConfigDb). One rule: DriverCredentialPolicy.DescribeOtherInstanceProblems +
DriverInstanceIdentities. Shared names and the driver's own id stay valid; env:
is not instance-scoped. Running drivers are unchanged (warn-only window: the
deploy gate and the upgrade preflight / /raw banner list them).

#836: OpcUaClient UserCertificatePath is a relative path inside
Drivers:OpcUaClient:UserCertificateDirectory (default
<LocalAppData>/OtOpcUa/pki/user-certificates). Syntactic rule (CredentialFilePaths,
a DriverCredentialPolicy path field) at every #815 seam; at load time
ResolveSecretRefsAsync joins, canonicalises and requires lexical AND
link-resolved containment before the PFX password is resolved, and
LoadCertificate refuses an unresolved path. A running driver's pre-#836 absolute
path keeps loading with a rate-limited warning (LegacyCredentialReferenceTracker.ReportPath).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-11-11 03:52:48 -05:00

56 lines
2.6 KiB
C#

using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Deployments;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Deployments;
/// <summary>
/// The upgrade preflight / <c>/raw</c> banner lists the configs the deploy gate will now refuse for lmxopcua#835
/// (another instance's secret) and #836 (absolute user-certificate path) — Draft and Deployed alike.
/// </summary>
[Trait("Category", "Unit")]
public sealed class CredentialReferencePreflightInstanceAndPathTests
{
private static DriverInstance Drv(string id, string name, string type, string config) =>
new() { DriverInstanceId = id, ClusterId = "c", Name = name, DriverType = type, DriverConfig = config };
[Fact]
public void Draft_and_deployed_cross_instance_references_are_listed()
{
var drivers = new[]
{
Drv("DRV-a", "A", DriverTypeNames.Galaxy, """{"Gateway":{"ApiKeySecretRef":"secret:galaxy/DRV-a/apikey"}}"""),
Drv("DRV-b", "B", DriverTypeNames.Galaxy, """{"Gateway":{"ApiKeySecretRef":"secret:galaxy/DRV-a/apikey"}}"""),
};
var devices = new[] { new Device { DeviceId = "dev-b", DriverInstanceId = "DRV-b", Name = "gw", DeviceConfig = """{"AlarmAck":{"FallbackPasswordSecretRef":"secret:otopcua/galaxy/A/ack"}}""" } };
var deployed = new[]
{
new DriverInstanceSpec(Guid.NewGuid(), "DRV-a", "A", DriverTypeNames.Galaxy, true, "{}"),
new DriverInstanceSpec(Guid.NewGuid(), "DRV-b", "B", DriverTypeNames.Galaxy, true, """{"Gateway":{"ApiKeySecretRef":"secret:galaxy/A/apikey"}}"""),
};
var findings = CredentialReferencePreflight.Scan(drivers, devices, deployed);
findings.Count.ShouldBe(3);
findings.ShouldAllBe(f => f.DriverInstanceId == "DRV-b" && f.Problem.Contains("lmxopcua#835"));
findings.Count(f => f.Source == CredentialFindingSource.Draft).ShouldBe(2);
findings.Single(f => f.Source == CredentialFindingSource.Deployed).DriverName.ShouldBe("B");
}
[Fact]
public void An_absolute_user_certificate_path_is_listed_without_the_path()
{
var findings = CredentialReferencePreflight.Scan(
[Drv("o", "Upstream", DriverTypeNames.OpcUaClient, """{"AuthType":"Certificate","UserCertificatePath":"C:\\certs\\plant.pfx"}""")],
[],
[]);
var f = findings.ShouldHaveSingleItem();
f.Problem.ShouldContain("lmxopcua#836");
f.Problem.ShouldNotContain("plant.pfx");
}
}