a224fd6391
A Designer could point a Galaxy / OpcUaClient endpoint at a host they control, set a credential field to any reference the node can resolve (env:ConnectionStrings__ConfigDb, env:ServerHistorian__ApiKey, secret:<any>, Galaxy file:<any path>) and click Test Connect / live browse; the node resolved the value and sent it to that host. One shared rule in Core.Abstractions (CredentialReferences + DriverCredentialPolicy) generalises the #803 Heidenhain namespace: - Galaxy: secret:galaxy/... | secret:otopcua/galaxy/... | env:OTOPCUA_GALAXY_... | legacy env:GALAXY_MXGW_...; file: refused; dev:/literal API key unchanged. - OpcUaClient: secret:opcuaclient/... | env:OTOPCUA_OPCUACLIENT_...; literals unchanged. - Heidenhain adopts the shared parser/resolver (namespace unchanged). Enforced at the AdminUI save path (DriverConfigSaveGate), the deploy gate (CredentialReferenceDeployChecks, code CredentialReferenceOutOfScope), Test Connect (AdminOperationsActor, before any probe), live browse (BrowserSessionService, before any browser) and again at resolve time in every driver/browser (fail closed, before any store/env read). Co-Authored-By: Claude <noreply@anthropic.com>
62 lines
3.6 KiB
C#
62 lines
3.6 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>lmxopcua#815: the credential-reference namespace deploy gate (Galaxy, OpcUaClient).</summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class DraftValidatorCredentialReferenceTests
|
|
{
|
|
private static IReadOnlyList<ValidationError> Validate(string type, string driverConfig, string deviceConfig = "{}") =>
|
|
DraftValidator.Validate(new DraftSnapshot
|
|
{
|
|
GenerationId = 1,
|
|
ClusterId = "c",
|
|
DriverInstances = [new DriverInstance { DriverInstanceId = "drv", ClusterId = "c", Name = "drv", DriverType = type, DriverConfig = driverConfig }],
|
|
Devices = [new Device { DeviceId = "dev", DriverInstanceId = "drv", Name = "dev1", DeviceConfig = deviceConfig }],
|
|
Tags = [],
|
|
}).Where(e => e.Code == CredentialReferenceDeployChecks.Code).ToList();
|
|
|
|
[Theory]
|
|
[InlineData("GalaxyMxGateway", """{"Gateway":{"Endpoint":"http://evil:5001","ApiKeySecretRef":"env:ConnectionStrings__ConfigDb"}}""")]
|
|
[InlineData("GalaxyMxGateway", """{"Gateway":{"ApiKeySecretRef":"env:ServerHistorian__ApiKey"}}""")]
|
|
[InlineData("GalaxyMxGateway", """{"Gateway":{"ApiKeySecretRef":"file:C:\\otopcua\\appsettings.json"}}""")]
|
|
[InlineData("GalaxyMxGateway", """{"AlarmAck":{"FallbackPasswordSecretRef":"secret:otopcua/apikeys/ab/galaxy-ack-password"}}""")]
|
|
[InlineData("OpcUaClient", """{"EndpointUrl":"opc.tcp://evil:4840","Password":"secret:otopcua/historian/api-key"}""")]
|
|
[InlineData("OpcUaClient", """{"UserCertificatePassword":"env:ConnectionStrings__ConfigDb"}""")]
|
|
public void An_out_of_namespace_reference_is_refused_naming_the_allowed_form(string type, string config)
|
|
{
|
|
var e = Validate(type, config).ShouldHaveSingleItem();
|
|
e.Context.ShouldBe("drv");
|
|
e.Message.ShouldContain("secret:");
|
|
e.Message.ShouldContain("lmxopcua#815");
|
|
e.Message.ShouldNotContain("ConnectionStrings__ConfigDb");
|
|
}
|
|
|
|
[Fact]
|
|
public void A_reference_in_a_device_config_is_checked_too()
|
|
=> Validate("GalaxyMxGateway", "{}", """{"Gateway":{"ApiKeySecretRef":"env:ConnectionStrings__ConfigDb"}}""")
|
|
.ShouldHaveSingleItem().Context.ShouldBe("dev");
|
|
|
|
[Theory]
|
|
[InlineData("GalaxyMxGateway", """{"Gateway":{"ApiKeySecretRef":"secret:galaxy/main/apikey"},"AlarmAck":{"FallbackPasswordSecretRef":"secret:otopcua/galaxy/ack-fallback"}}""")]
|
|
[InlineData("GalaxyMxGateway", """{"Gateway":{"ApiKeySecretRef":"env:GALAXY_MXGW_API_KEY"}}""")]
|
|
[InlineData("GalaxyMxGateway", """{"Gateway":{"ApiKeySecretRef":"dev:k"}}""")]
|
|
[InlineData("OpcUaClient", """{"Password":"literal","UserCertificatePassword":"env:OTOPCUA_OPCUACLIENT_CERT_PW"}""")]
|
|
[InlineData("Modbus", """{"Password":"env:ConnectionStrings__ConfigDb"}""")]
|
|
public void Namespaced_literal_and_unrelated_configs_pass(string type, string config)
|
|
=> Validate(type, config).ShouldBeEmpty();
|
|
|
|
[Fact]
|
|
public void Heidenhain_is_reported_once_by_its_own_gate_not_twice()
|
|
=> DraftValidator.Validate(new DraftSnapshot
|
|
{
|
|
GenerationId = 1, ClusterId = "c",
|
|
DriverInstances = [new DriverInstance { DriverInstanceId = "hh", ClusterId = "c", Name = "hh", DriverType = "Heidenhain", DriverConfig = """{"Access":{"InspectPassword":"env:ConnectionStrings__ConfigDb"}}""" }],
|
|
Devices = [], Tags = [],
|
|
})
|
|
.Count(e => e.Code is CredentialReferenceDeployChecks.Code or HeidenhainDeployChecks.CredentialCode).ShouldBe(1);
|
|
}
|