4563940792
User decision: save, Test Connect, live browse and the deploy gate keep refusing out-of-namespace credential references, but an ALREADY-DEPLOYED driver must not fault at the upgrade restart or (Galaxy) at the next gateway reconnect, since ReopenAsync re-resolves Gateway.ApiKeySecretRef. - LegacyCredentialReferenceTracker (Core.Abstractions): the one, deprecated, call-site-chosen compatibility path. Running Galaxy (InitializeAsync, ReopenAsync, ack fallback) and OpcUaClient (session open) pass it and keep the pre-#815 semantics (case-insensitive prefixes, Galaxy file:, OpcUaClient literal pass-through) with a rate-limited Warning (driver/field/allowed form, never the value) and a credential-compat:<field> diagnostics entry (both drivers now implement IDriverDiagnostics). Browsers and probes pass none and stay hard-fail; Heidenhain has none. - Deploy-gate message names the migration. - Upgrade preflight: CredentialReferencePreflightService (admin nodes, once after ApplicationStarted, ConfigDb only, resolves nothing) logs a Warning per Draft row / Deployed-artifact finding; the /raw banner shows them plus Running drivers reporting warn-only use (driver-health diagnostics). - docs/security.md: exact replacement table (incl. the old form default env:MX_API_KEY, file:, un-namespaced secret:, casing changes), the ReopenAsync reconnect behaviour, the warn-only window and its removal. Co-Authored-By: Claude <noreply@anthropic.com>
63 lines
3.7 KiB
C#
63 lines
3.7 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.ShouldContain("Migrate it before deploying");
|
|
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);
|
|
}
|