63331b0416
#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>
92 lines
4.4 KiB
C#
92 lines
4.4 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Validation;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Configuration.Tests;
|
|
|
|
/// <summary>
|
|
/// The deploy gate for lmxopcua#835 (a credential reference naming ANOTHER driver instance's secret) and #836 (the
|
|
/// OpcUaClient user-certificate path confined to the node's certificate directory).
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class DraftValidatorCredentialInstanceAndPathTests
|
|
{
|
|
private static IReadOnlyList<ValidationError> Validate(params DriverInstance[] drivers) =>
|
|
Validate(drivers, []);
|
|
|
|
private static IReadOnlyList<ValidationError> Validate(DriverInstance[] drivers, Device[] devices) =>
|
|
DraftValidator.Validate(new DraftSnapshot
|
|
{
|
|
GenerationId = 1,
|
|
ClusterId = "c",
|
|
DriverInstances = drivers,
|
|
Devices = devices,
|
|
Tags = [],
|
|
}).Where(e => e.Code is CredentialReferenceDeployChecks.Code or CredentialReferenceDeployChecks.OtherInstanceCode).ToList();
|
|
|
|
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 A_galaxy_driver_naming_another_instances_key_is_refused()
|
|
{
|
|
var e = Validate(
|
|
Drv("DRV-victim", "Victim", "GalaxyMxGateway", """{"Gateway":{"ApiKeySecretRef":"secret:galaxy/DRV-victim/apikey"}}"""),
|
|
Drv("DRV-evil", "Evil", "GalaxyMxGateway", """{"Gateway":{"Endpoint":"http://evil:5001","ApiKeySecretRef":"secret:galaxy/DRV-victim/apikey"}}"""))
|
|
.ShouldHaveSingleItem();
|
|
e.Code.ShouldBe(CredentialReferenceDeployChecks.OtherInstanceCode);
|
|
e.Context.ShouldBe("DRV-evil");
|
|
e.Message.ShouldContain("lmxopcua#835");
|
|
}
|
|
|
|
[Fact]
|
|
public void A_device_config_naming_another_instance_by_name_is_refused() =>
|
|
Validate(
|
|
[
|
|
Drv("DRV-a", "Victim", "OpcUaClient", "{}"),
|
|
Drv("DRV-b", "Evil", "OpcUaClient", "{}"),
|
|
],
|
|
[new Device { DeviceId = "dev-b", DriverInstanceId = "DRV-b", Name = "d", DeviceConfig = """{"Password":"secret:opcuaclient/victim/pw"}""" }])
|
|
.ShouldHaveSingleItem().Context.ShouldBe("dev-b");
|
|
|
|
[Fact]
|
|
public void Shared_and_own_keys_pass() =>
|
|
Validate(
|
|
Drv("DRV-a", "A", "GalaxyMxGateway", """{"Gateway":{"ApiKeySecretRef":"secret:galaxy/DRV-a/apikey"}}"""),
|
|
Drv("DRV-b", "B", "GalaxyMxGateway", """{"Gateway":{"ApiKeySecretRef":"secret:galaxy/gw/apikey"}}"""),
|
|
Drv("DRV-c", "C", "GalaxyMxGateway", """{"Gateway":{"ApiKeySecretRef":"secret:galaxy/gw/apikey"}}"""))
|
|
.ShouldBeEmpty();
|
|
|
|
[Fact]
|
|
public void Heidenhain_cross_instance_is_reported_by_this_gate() =>
|
|
DraftValidator.Validate(new DraftSnapshot
|
|
{
|
|
GenerationId = 1, ClusterId = "c",
|
|
DriverInstances =
|
|
[
|
|
Drv("H1", "line1", "Heidenhain", """{"Access":{"InspectPassword":"secret:heidenhain/line1/inspect"}}"""),
|
|
Drv("H2", "line2", "Heidenhain", """{"Access":{"InspectPassword":"secret:heidenhain/line1/inspect"}}"""),
|
|
],
|
|
Devices = [], Tags = [],
|
|
})
|
|
.Where(e => e.Code == CredentialReferenceDeployChecks.OtherInstanceCode)
|
|
.ShouldHaveSingleItem().Context.ShouldBe("H2");
|
|
|
|
[Theory]
|
|
[InlineData("""{"AuthType":"Certificate","UserCertificatePath":"C:\\Windows\\System32\\config\\systemprofile\\host.pfx"}""")]
|
|
[InlineData("""{"UserCertificatePath":"/etc/ssl/private/host.pfx"}""")]
|
|
[InlineData("""{"UserCertificatePath":"plant1/../../host.pfx"}""")]
|
|
public void An_opcuaclient_user_certificate_path_outside_the_directory_is_refused(string config)
|
|
{
|
|
var e = Validate(Drv("DRV-o", "o", "OpcUaClient", config)).ShouldHaveSingleItem();
|
|
e.Code.ShouldBe(CredentialReferenceDeployChecks.Code);
|
|
e.Message.ShouldContain("lmxopcua#836");
|
|
e.Message.ShouldNotContain("host.pfx");
|
|
}
|
|
|
|
[Fact]
|
|
public void A_relative_user_certificate_path_passes() =>
|
|
Validate(Drv("DRV-o", "o", "OpcUaClient", """{"AuthType":"Certificate","UserCertificatePath":"plant1/user.pfx"}""")).ShouldBeEmpty();
|
|
}
|