using Shouldly; using Xunit; namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests; /// /// lmxopcua#835: inside one namespace, a credential reference whose instance segment /// (secret:galaxy/<X>/…) names ANOTHER existing instance of the same driver type is refused. /// [Trait("Category", "Unit")] public sealed class CredentialInstanceScopeTests { private static readonly DriverInstanceIdentity[] Instances = [ new("DRV-aaa", "PlantA", DriverTypeNames.Galaxy), new("DRV-bbb", "PlantB", DriverTypeNames.Galaxy), new("DRV-ccc", "UpstreamC", DriverTypeNames.OpcUaClient), new("DRV-ddd", "UpstreamD", DriverTypeNames.OpcUaClient), ]; private static string Galaxy(string apiKeyRef) => "{\"Gateway\":{\"Endpoint\":\"http://evil:5001\",\"ApiKeySecretRef\":" + System.Text.Json.JsonSerializer.Serialize(apiKeyRef) + "}}"; [Theory] [InlineData("secret:galaxy/DRV-bbb/apikey")] [InlineData("secret:galaxy/PlantB/apikey")] [InlineData("secret:galaxy/plantb/apikey")] // case-insensitive [InlineData("secret:otopcua/galaxy/PlantB/apikey")] // the second namespace [InlineData("${secret:galaxy/PlantB/apikey}")] // the token spelling [InlineData("secret:galaxy/PlantB")] // the segment alone public void A_reference_naming_another_instance_is_refused(string reference) { var problem = DriverCredentialPolicy .DescribeOtherInstanceProblems(DriverTypeNames.Galaxy, Galaxy(reference), "DRV-aaa", Instances) .ShouldHaveSingleItem(); problem.ShouldContain("Gateway.ApiKeySecretRef"); problem.ShouldContain("lmxopcua#835"); problem.ShouldContain("DRV-bbb"); } [Theory] [InlineData("secret:galaxy/DRV-aaa/apikey")] // its own id [InlineData("secret:galaxy/PlantA/apikey")] // its own name [InlineData("secret:galaxy/gw/apikey")] // a shared name no instance uses [InlineData("secret:galaxy/apikey")] [InlineData("secret:galaxy/UpstreamC/apikey")] // another TYPE's instance — not this namespace's owner [InlineData("env:OTOPCUA_GALAXY_PLANTA_KEY")] // its own name in env form (lmxopcua#849) [InlineData("env:OTOPCUA_GALAXY_DRV_AAA_KEY")] // its own id in env form [InlineData("env:GALAXY_MXGW_API_KEY")] // a shared variable that names no instance [InlineData("env:OTOPCUA_GALAXY_PLANTBX_KEY")] // 'PLANTB' only at a '_' boundary [InlineData("dev:k")] [InlineData("")] public void Own_shared_and_non_secret_references_pass(string reference) => DriverCredentialPolicy.DescribeOtherInstanceProblems(DriverTypeNames.Galaxy, Galaxy(reference), "DRV-aaa", Instances) .ShouldBeEmpty(); [Fact] public void A_driver_being_created_may_name_no_existing_instance() => DriverCredentialPolicy.DescribeOtherInstanceProblems(DriverTypeNames.Galaxy, Galaxy("secret:galaxy/PlantA/apikey"), null, Instances) .ShouldHaveSingleItem(); [Fact] public void Renaming_ones_own_driver_after_the_victim_does_not_unlock_the_victims_secret() { // Attacker DRV-zzz renamed itself "PlantB" (names are unique per folder, not fleet-wide). var withTwin = Instances.Append(new DriverInstanceIdentity("DRV-zzz", "PlantB", DriverTypeNames.Galaxy)); DriverCredentialPolicy.DescribeOtherInstanceProblems(DriverTypeNames.Galaxy, Galaxy("secret:galaxy/PlantB/apikey"), "DRV-zzz", withTwin) .ShouldHaveSingleItem(); } [Fact] public void The_ack_password_and_device_level_fields_are_checked_too() { DriverCredentialPolicy.DescribeOtherInstanceProblems( DriverTypeNames.Galaxy, """{"AlarmAck":{"FallbackPasswordSecretRef":"secret:otopcua/galaxy/DRV-bbb/ack"}}""", "DRV-aaa", Instances) .ShouldHaveSingleItem().ShouldContain("AlarmAck.FallbackPasswordSecretRef"); DriverCredentialPolicy.DescribeOtherInstanceProblems( DriverTypeNames.Galaxy, """{"Devices":[{"Gateway":{"ApiKeySecretRef":"secret:galaxy/PlantB/k"}}]}""", "DRV-aaa", Instances) .ShouldHaveSingleItem().ShouldContain("Devices[0].Gateway.ApiKeySecretRef"); } [Theory] [InlineData("""{"Password":"secret:opcuaclient/UpstreamD/pw"}""", 1)] [InlineData("""{"UserCertificatePassword":"secret:opcuaclient/DRV-ddd/pfx"}""", 1)] [InlineData("""{"Password":"secret:opcuaclient/UpstreamC/pw"}""", 0)] [InlineData("""{"Password":"secret:opcuaclient/shared/pw"}""", 0)] public void OpcUaClient_has_the_same_rule(string config, int expected) => DriverCredentialPolicy.DescribeOtherInstanceProblems(DriverTypeNames.OpcUaClient, config, "DRV-ccc", Instances) .Count.ShouldBe(expected); [Fact] public void Heidenhain_has_the_same_rule() { IEnumerable hh = [new("H1", "line1", DriverTypeNames.Heidenhain), new("H2", "line2", DriverTypeNames.Heidenhain)]; DriverCredentialPolicy.DescribeOtherInstanceProblems( DriverTypeNames.Heidenhain, """{"Access":{"InspectPassword":"secret:heidenhain/line2/inspect"}}""", "H1", hh) .ShouldHaveSingleItem(); } [Fact] public void HasInstanceSegments_gates_the_ConfigDb_read() { DriverCredentialPolicy.HasInstanceSegments(DriverTypeNames.Galaxy, Galaxy("secret:galaxy/x/apikey")).ShouldBeTrue(); DriverCredentialPolicy.HasInstanceSegments(DriverTypeNames.Galaxy, Galaxy("env:OTOPCUA_GALAXY_X")).ShouldBeTrue(); // #849 DriverCredentialPolicy.HasInstanceSegments(DriverTypeNames.Galaxy, Galaxy("env:ConnectionStrings__ConfigDb")).ShouldBeFalse(); DriverCredentialPolicy.HasInstanceSegments(DriverTypeNames.Modbus, """{"Password":"secret:galaxy/x"}""").ShouldBeFalse(); DriverCredentialPolicy.HasInstanceSegments(DriverTypeNames.Galaxy, "not json").ShouldBeFalse(); } [Fact] public void The_combined_overload_reports_both_rules() { var config = """{"Gateway":{"ApiKeySecretRef":"secret:galaxy/PlantB/k"},"AlarmAck":{"FallbackPasswordSecretRef":"env:ConnectionStrings__ConfigDb"}}"""; var problems = DriverCredentialPolicy.DescribeProblems(DriverTypeNames.Galaxy, config, "DRV-aaa", Instances); problems.Count.ShouldBe(2); problems.ShouldContain(p => p.Contains("lmxopcua#815")); problems.ShouldContain(p => p.Contains("lmxopcua#835")); } // --------------------------------------------------------------------------------------------------------------- // lmxopcua#849 — env: references follow the same per-instance rule (env:_…) // --------------------------------------------------------------------------------------------------------------- [Theory] [InlineData("env:OTOPCUA_GALAXY_PLANTB_KEY")] // another instance's name, upper-cased [InlineData("env:OTOPCUA_GALAXY_PLANTB")] // the part alone [InlineData("env:OTOPCUA_GALAXY_DRV_BBB_KEY")] // another instance's id, '-' as '_' [InlineData("env:GALAXY_MXGW_PLANTB_API_KEY")] // the legacy prefix too public void An_env_reference_naming_another_instance_is_refused(string reference) { var problem = DriverCredentialPolicy .DescribeOtherInstanceProblems(DriverTypeNames.Galaxy, Galaxy(reference), "DRV-aaa", Instances) .ShouldHaveSingleItem(); problem.ShouldContain("lmxopcua#849"); problem.ShouldContain("DRV-bbb"); problem.ShouldContain("Gateway.ApiKeySecretRef"); } [Fact] public void An_env_match_against_another_instance_wins_over_the_owners() { // Owner "Line 1" (LINE_1) and another instance "Line" (LINE): OTOPCUA_GALAXY_LINE_1_KEY names both. IEnumerable lines = [new("DRV-1", "Line 1", DriverTypeNames.Galaxy), new("DRV-2", "Line", DriverTypeNames.Galaxy)]; DriverCredentialPolicy.DescribeOtherInstanceProblems(DriverTypeNames.Galaxy, Galaxy("env:OTOPCUA_GALAXY_LINE_1_KEY"), "DRV-1", lines) .ShouldHaveSingleItem(); // …while the owner's id-scoped variable names only the owner. DriverCredentialPolicy.DescribeOtherInstanceProblems(DriverTypeNames.Galaxy, Galaxy("env:OTOPCUA_GALAXY_DRV_1_KEY"), "DRV-1", lines) .ShouldBeEmpty(); } [Fact] public void OpcUaClient_and_Heidenhain_env_forms_have_the_rule() { DriverCredentialPolicy.DescribeOtherInstanceProblems( DriverTypeNames.OpcUaClient, """{"Password":"env:OTOPCUA_OPCUACLIENT_UPSTREAMD_PW"}""", "DRV-ccc", Instances) .ShouldHaveSingleItem().ShouldContain("lmxopcua#849"); IEnumerable hh = [new("H1", "line1", DriverTypeNames.Heidenhain), new("H2", "line2", DriverTypeNames.Heidenhain)]; DriverCredentialPolicy.DescribeOtherInstanceProblems( DriverTypeNames.Heidenhain, """{"Access":{"DncPassword":"env:OTOPCUA_HEIDENHAIN_LINE2_DNC"}}""", "H1", hh) .ShouldHaveSingleItem(); DriverCredentialPolicy.DescribeOtherInstanceProblems( DriverTypeNames.Heidenhain, """{"Access":{"DncPassword":"env:OTOPCUA_HEIDENHAIN_DNC_PASSWORD"}}""", "H1", hh) .ShouldBeEmpty(); } [Theory] [InlineData("DRV-1a2b", "DRV_1A2B")] [InlineData("Line 1", "LINE_1")] [InlineData("plant.b", "PLANT_B")] [InlineData("---", "")] [InlineData("", "")] public void Identifiers_normalise_to_environment_form(string identifier, string expected) => DriverCredentialPolicy.NormalizeEnvironmentIdentifier(identifier).ShouldBe(expected); [Fact] public void Instance_references_are_collected_for_both_kinds() { var refs = DriverCredentialPolicy.CollectInstanceReferences(DriverTypeNames.Galaxy, """{"Gateway":{"ApiKeySecretRef":"secret:galaxy/gw/k"},"AlarmAck":{"FallbackPasswordSecretRef":"env:OTOPCUA_GALAXY_ACK_PW"}}"""); refs.Count.ShouldBe(2); refs[0].ShouldBe(new InstanceReference("Gateway.ApiKeySecretRef", CredentialReferenceKind.Secret, "gw")); refs[1].ShouldBe(new InstanceReference("AlarmAck.FallbackPasswordSecretRef", CredentialReferenceKind.Environment, "ACK_PW")); DriverCredentialPolicy.ReferenceNamesName(refs[1], "ack").ShouldBeTrue(); DriverCredentialPolicy.ReferenceNamesName(refs[1], "ac").ShouldBeFalse(); DriverCredentialPolicy.ReferenceNamesName(refs[0], "GW").ShouldBeTrue(); } // --------------------------------------------------------------------------------------------------------------- // #849 review — a driver NAMED 'drv' (or id-shaped) must not shadow the id convention // --------------------------------------------------------------------------------------------------------------- private const string RealId = "DRV-0123456789ab"; // the RawTreeService.NewId shape private static readonly DriverInstanceIdentity[] WithDrvNamed = [ new(RealId, "PlantA", DriverTypeNames.Galaxy), new("DRV-fedcba987654", "drv", DriverTypeNames.Galaxy), // a pre-existing driver named 'drv' new("DRV-111111111111", "DRV-0123456789AB", DriverTypeNames.Galaxy), // …or named like another's id ]; [Theory] [InlineData("env:OTOPCUA_GALAXY_DRV_0123456789AB_KEY")] [InlineData("env:OTOPCUA_GALAXY_DRV_0123456789AB")] [InlineData("env:GALAXY_MXGW_DRV_0123456789AB_API_KEY")] [InlineData("secret:galaxy/DRV-0123456789ab/apikey")] public void An_id_form_reference_is_matched_by_id_only_so_a_pre_existing_drv_name_does_not_break_it(string reference) => DriverCredentialPolicy.DescribeOtherInstanceProblems(DriverTypeNames.Galaxy, Galaxy(reference), RealId, WithDrvNamed) .ShouldBeEmpty(); [Fact] public void An_id_form_reference_still_names_the_other_instance_by_its_id() => DriverCredentialPolicy.DescribeOtherInstanceProblems( DriverTypeNames.Galaxy, Galaxy("env:OTOPCUA_GALAXY_DRV_0123456789AB_KEY"), "DRV-fedcba987654", WithDrvNamed) .ShouldHaveSingleItem().ShouldContain(RealId); [Fact] public void A_drv_name_still_matches_a_reference_that_is_not_id_shaped() => // 'DRV_FOO' is not an id form, so the name 'drv' does claim it (the create/rename check refuses that name anyway). DriverCredentialPolicy.DescribeOtherInstanceProblems(DriverTypeNames.Galaxy, Galaxy("env:OTOPCUA_GALAXY_DRV_FOO_KEY"), RealId, WithDrvNamed) .ShouldHaveSingleItem(); [Theory] [InlineData("env:OTOPCUA_GALAXY_DRV_0123456789AB_KEY", true)] [InlineData("env:OTOPCUA_GALAXY_DRV_0123456789AB", true)] [InlineData("env:OTOPCUA_GALAXY_DRV_0123456789ABC_KEY", false)] // 13 hex: not an id form [InlineData("env:OTOPCUA_GALAXY_DRV_0123456789AG_KEY", false)] // not hex [InlineData("env:OTOPCUA_GALAXY_DRV_KEY", false)] [InlineData("secret:galaxy/DRV-0123456789ab/k", true)] [InlineData("secret:galaxy/drv-0123456789AB/k", true)] [InlineData("secret:galaxy/DRV-0123456789ab-x/k", false)] public void Id_shape_detection(string reference, bool expected) => DriverCredentialPolicy.IsIdShaped(DriverCredentialPolicy.CollectInstanceReferences(DriverTypeNames.Galaxy, Galaxy(reference)).Single()) .ShouldBe(expected); [Theory] [InlineData("drv", true)] [InlineData("DRV", true)] [InlineData("drv-", false)] // env form 'DRV_' — neither 'DRV' nor an id form [InlineData("DRV-0123456789ab", true)] [InlineData("drv 0123456789AB extra", true)] [InlineData("DRV-0123456789", false)] [InlineData("Driver", false)] [InlineData("PlantA", false)] public void Reserved_instance_names(string name, bool expected) => DriverCredentialPolicy.IsReservedInstanceName(name).ShouldBe(expected); }