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; /// /// 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). /// [Trait("Category", "Unit")] public sealed class DraftValidatorCredentialInstanceAndPathTests { private static IReadOnlyList Validate(params DriverInstance[] drivers) => Validate(drivers, []); private static IReadOnlyList 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"); /// lmxopcua#849: the env: form carries the same per-instance rule, under the same code. [Fact] public void A_galaxy_env_reference_naming_another_instance_is_refused() { var e = Validate( Drv("DRV-victim", "Victim", "GalaxyMxGateway", """{"Gateway":{"ApiKeySecretRef":"env:OTOPCUA_GALAXY_VICTIM_KEY"}}"""), Drv("DRV-evil", "Evil", "GalaxyMxGateway", """{"Gateway":{"Endpoint":"http://evil:5001","ApiKeySecretRef":"env:GALAXY_MXGW_DRV_VICTIM_KEY"}}""")) .ShouldHaveSingleItem(); e.Code.ShouldBe(CredentialReferenceDeployChecks.OtherInstanceCode); e.Context.ShouldBe("DRV-evil"); e.Message.ShouldContain("lmxopcua#849"); } [Fact] public void Shared_and_own_env_variables_pass() => Validate( Drv("DRV-a", "A", "GalaxyMxGateway", """{"Gateway":{"ApiKeySecretRef":"env:OTOPCUA_GALAXY_DRV_A_KEY"}}"""), Drv("DRV-b", "B", "GalaxyMxGateway", """{"Gateway":{"ApiKeySecretRef":"env:GALAXY_MXGW_API_KEY"}}"""), Drv("DRV-c", "C", "GalaxyMxGateway", """{"Gateway":{"ApiKeySecretRef":"env:GALAXY_MXGW_API_KEY"}}""")) .ShouldBeEmpty(); /// #849 review: a pre-existing driver named 'drv' must not fail the id-form variables of its siblings. [Fact] public void A_driver_named_drv_does_not_break_id_form_env_variables() => Validate( Drv("DRV-0123456789ab", "A", "GalaxyMxGateway", """{"Gateway":{"ApiKeySecretRef":"env:OTOPCUA_GALAXY_DRV_0123456789AB_KEY"}}"""), Drv("DRV-fedcba987654", "drv", "GalaxyMxGateway", """{"Gateway":{"ApiKeySecretRef":"env:OTOPCUA_GALAXY_DRV_FEDCBA987654_KEY"}}""")) .ShouldBeEmpty(); [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"); } /// Review L-1: a path in an object that explicitly switches certificate auth off is not checked… [Fact] public void A_path_under_explicit_non_certificate_auth_passes() => Validate(Drv("DRV-o", "o", "OpcUaClient", """{"AuthType":"Anonymous","UserCertificatePath":"C:\\stale\\host.pfx"}""")).ShouldBeEmpty(); /// …but the driver/device split cannot smuggle one past the gate: the runtime-merged config is checked too. [Fact] public void A_path_split_from_its_certificate_switch_is_caught_on_the_merged_config() { var e = Validate( [Drv("DRV-o", "o", "OpcUaClient", """{"AuthType":"Anonymous","UserCertificatePath":"C:\\stolen\\host.pfx"}""")], [new Device { DeviceId = "dev-o", DriverInstanceId = "DRV-o", Name = "d", DeviceConfig = """{"AuthType":"Certificate"}""" }]) .ShouldHaveSingleItem(); e.Context.ShouldBe("DRV-o"); e.Message.ShouldContain("merged with its devices"); e.Message.ShouldContain("lmxopcua#836"); } [Fact] public void A_relative_user_certificate_path_passes() => Validate(Drv("DRV-o", "o", "OpcUaClient", """{"AuthType":"Certificate","UserCertificatePath":"plant1/user.pfx"}""")).ShouldBeEmpty(); }