using Shouldly; using Xunit; namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests; /// lmxopcua#815 — the shared credential-reference parser, resolver and per-driver policy table. public sealed class CredentialReferencesTests { private static readonly CredentialScope Galaxy = DriverCredentialPolicy.GalaxyScope; [Theory] [InlineData(null, CredentialReferenceKind.Blank)] [InlineData("", CredentialReferenceKind.Blank)] [InlineData("secret:galaxy/inst/apikey", CredentialReferenceKind.Secret)] [InlineData("${secret:galaxy/inst/apikey}", CredentialReferenceKind.Secret)] [InlineData("secret:otopcua/galaxy/ack-fallback", CredentialReferenceKind.Secret)] [InlineData("env:OTOPCUA_GALAXY_API_KEY", CredentialReferenceKind.Environment)] [InlineData("env:GALAXY_MXGW_API_KEY", CredentialReferenceKind.Environment)] [InlineData("plain-key", CredentialReferenceKind.Literal)] [InlineData("dev:k", CredentialReferenceKind.Literal)] [InlineData("secret:otopcua/historian/api-key", CredentialReferenceKind.OutOfScope)] [InlineData("secret:otopcua/apikeys/ab/galaxy-ack-password", CredentialReferenceKind.OutOfScope)] [InlineData("secret:OT-MESH-PSK-MAIN", CredentialReferenceKind.OutOfScope)] [InlineData("secret:galaxy/", CredentialReferenceKind.OutOfScope)] [InlineData("secret:galaxy/../otopcua/historian/api-key", CredentialReferenceKind.OutOfScope)] [InlineData("secret:galaxy//x", CredentialReferenceKind.OutOfScope)] [InlineData("secret:galaxyx/y", CredentialReferenceKind.OutOfScope)] [InlineData("${secret:galaxy/x", CredentialReferenceKind.OutOfScope)] [InlineData("SECRET:galaxy/x", CredentialReferenceKind.OutOfScope)] [InlineData("Env:OTOPCUA_GALAXY_X", CredentialReferenceKind.OutOfScope)] [InlineData("env:ConnectionStrings__ConfigDb", CredentialReferenceKind.OutOfScope)] [InlineData("env:ServerHistorian__ApiKey", CredentialReferenceKind.OutOfScope)] [InlineData("env:OTOPCUA_GALAXY_", CredentialReferenceKind.OutOfScope)] [InlineData("env:OTOPCUA_GALAXY_lower", CredentialReferenceKind.OutOfScope)] [InlineData("env:OTOPCUA_HEIDENHAIN_X", CredentialReferenceKind.OutOfScope)] [InlineData("file:appsettings.json", CredentialReferenceKind.OutOfScope)] [InlineData("FILE:x", CredentialReferenceKind.OutOfScope)] public void Parse_classifies_against_the_galaxy_scope(string? value, CredentialReferenceKind expected) => CredentialReferences.Parse(value, Galaxy, out _).ShouldBe(expected); [Fact] public void Scopes_do_not_overlap_across_drivers() { CredentialReferences.Parse("secret:heidenhain/x", Galaxy, out _).ShouldBe(CredentialReferenceKind.OutOfScope); CredentialReferences.Parse("secret:galaxy/x", DriverCredentialPolicy.OpcUaClientScope, out _) .ShouldBe(CredentialReferenceKind.OutOfScope); CredentialReferences.Parse("env:OTOPCUA_GALAXY_X", DriverCredentialPolicy.HeidenhainScope, out _) .ShouldBe(CredentialReferenceKind.OutOfScope); } [Theory] [InlineData(CredentialLiteralPolicy.Allowed, "plain", true)] [InlineData(CredentialLiteralPolicy.Refused, "plain", false)] [InlineData(CredentialLiteralPolicy.Refused, "dev:x", false)] [InlineData(CredentialLiteralPolicy.DevPrefixOnly, "dev:x", true)] [InlineData(CredentialLiteralPolicy.DevPrefixOnly, "plain", false)] [InlineData(CredentialLiteralPolicy.Allowed, "env:ConnectionStrings__ConfigDb", false)] public void IsAcceptable_applies_the_literal_policy(CredentialLiteralPolicy policy, string value, bool expected) => CredentialReferences.IsAcceptable(value, Galaxy, policy).ShouldBe(expected); /// The fail-closed core: an out-of-scope reference throws before either source is consulted. [Theory] [InlineData("env:ConnectionStrings__ConfigDb")] [InlineData("secret:otopcua/historian/api-key")] [InlineData("file:appsettings.json")] public async Task Resolve_never_reads_for_an_out_of_scope_reference(string value) { var reads = 0; var ex = await Should.ThrowAsync(async () => await CredentialReferences.ResolveAsync( value, Galaxy, "F", CredentialLiteralPolicy.Allowed, (_, _) => { reads++; return ValueTask.FromResult("LEAKED"); }, _ => { reads++; return "LEAKED"; }, CancellationToken.None)); reads.ShouldBe(0); ex.Message.ShouldContain("'secret:galaxy/'"); ex.Message.ShouldContain("'env:OTOPCUA_GALAXY_'"); ex.Message.ShouldNotContain("LEAKED"); } [Fact] public async Task Resolve_reads_scoped_references_and_fails_closed_when_absent() { (await CredentialReferences.ResolveAsync("secret:galaxy/a", Galaxy, "F", CredentialLiteralPolicy.Refused, (n, _) => ValueTask.FromResult(n == "galaxy/a" ? "v" : null), _ => null, CancellationToken.None)) .ShouldBe("v"); (await CredentialReferences.ResolveAsync("env:OTOPCUA_GALAXY_A", Galaxy, "F", CredentialLiteralPolicy.Refused, null, n => n == "OTOPCUA_GALAXY_A" ? "e" : null, CancellationToken.None)).ShouldBe("e"); (await Should.ThrowAsync(async () => await CredentialReferences.ResolveAsync( "secret:galaxy/missing", Galaxy, "F", CredentialLiteralPolicy.Refused, (_, _) => ValueTask.FromResult(null), null, CancellationToken.None))).Message.ShouldContain("absent"); (await Should.ThrowAsync(async () => await CredentialReferences.ResolveAsync( "env:OTOPCUA_GALAXY_UNSET", Galaxy, "F", CredentialLiteralPolicy.Refused, null, _ => null, CancellationToken.None))).Message.ShouldContain("not set"); (await Should.ThrowAsync(async () => await CredentialReferences.ResolveAsync( "secret:galaxy/a", Galaxy, "F", CredentialLiteralPolicy.Refused, null, null, CancellationToken.None))).Message.ShouldContain("no secret store"); } [Fact] public void DescribeProblems_scans_every_depth_case_insensitively_and_never_echoes_the_value() { const string json = """ { "gateway": { "endpoint": "http://evil:5001", "apiKeySecretRef": "env:ConnectionStrings__ConfigDb" }, "Devices": [ { "AlarmAck": { "FallbackPasswordSecretRef": "hunter2" } } ] } """; var problems = DriverCredentialPolicy.DescribeProblems(DriverTypeNames.Galaxy, json); problems.Count.ShouldBe(2); problems.ShouldContain(p => p.StartsWith("gateway.apiKeySecretRef", StringComparison.Ordinal)); problems.ShouldContain(p => p.StartsWith("Devices[0].AlarmAck.FallbackPasswordSecretRef", StringComparison.Ordinal)); problems.ShouldAllBe(p => !p.Contains("hunter2") && !p.Contains("ConnectionStrings__ConfigDb")); } [Theory] [InlineData(DriverTypeNames.Galaxy, """{"Gateway":{"ApiKeySecretRef":"secret:galaxy/main/apikey"},"AlarmAck":{"FallbackPasswordSecretRef":"dev:x"}}""")] [InlineData(DriverTypeNames.Galaxy, """{"Gateway":{"ApiKeySecretRef":"literal-key"}}""")] [InlineData(DriverTypeNames.OpcUaClient, """{"Password":"plain","UserCertificatePassword":"secret:opcuaclient/plant/cert"}""")] [InlineData(DriverTypeNames.Heidenhain, """{"Access":{"InspectPassword":"env:OTOPCUA_HEIDENHAIN_INSPECT"}}""")] [InlineData(DriverTypeNames.Modbus, """{"Password":"env:ConnectionStrings__ConfigDb"}""")] [InlineData(DriverTypeNames.Galaxy, "")] public void DescribeProblems_accepts_namespaced_and_unrelated_configs(string driverType, string json) => DriverCredentialPolicy.DescribeProblems(driverType, json).ShouldBeEmpty(); [Theory] [InlineData(DriverTypeNames.OpcUaClient, """{"Password":"secret:galaxy/main/apikey"}""")] [InlineData(DriverTypeNames.OpcUaClient, """{"UserCertificatePassword":"env:ServerHistorian__ApiKey"}""")] [InlineData(DriverTypeNames.Galaxy, """{"Gateway":{"ApiKeySecretRef":"file:C:\\ProgramData\\x.txt"}}""")] [InlineData(DriverTypeNames.Galaxy, """{"Gateway":{"ApiKeySecretRef":42}}""")] [InlineData(DriverTypeNames.Heidenhain, """{"InspectPassword":"plain"}""")] [InlineData(DriverTypeNames.Galaxy, "{not json")] public void DescribeProblems_refuses_out_of_namespace_configs(string driverType, string json) => DriverCredentialPolicy.DescribeProblems(driverType, json).ShouldNotBeEmpty(); }