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>
68 lines
3.5 KiB
C#
68 lines
3.5 KiB
C#
using Akka.Actor;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Admin;
|
|
using ZB.MOM.WW.OtOpcUa.ControlPlane.AdminOperations;
|
|
using ZB.MOM.WW.OtOpcUa.ControlPlane.Tests.Harness;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.ControlPlane.Tests;
|
|
|
|
/// <summary>
|
|
/// lmxopcua#815: Test Connect with a credential reference outside the driver's namespace is refused BEFORE any
|
|
/// probe runs — the probe is the seam that would resolve the reference (store / environment) and send the value
|
|
/// to the author-chosen endpoint, so "the probe was never invoked" is "nothing was read".
|
|
/// </summary>
|
|
public sealed class AdminOperationsActorCredentialScopeTests : ControlPlaneActorTestBase
|
|
{
|
|
[Theory]
|
|
[InlineData(DriverTypeNames.Galaxy, """{"Gateway":{"Endpoint":"http://attacker:5001","ApiKeySecretRef":"env:ConnectionStrings__ConfigDb"}}""")]
|
|
[InlineData(DriverTypeNames.Galaxy, """{"Gateway":{"Endpoint":"http://attacker:5001","ApiKeySecretRef":"env:MX_API_KEY"}}""")]
|
|
[InlineData(DriverTypeNames.Galaxy, """{"Gateway":{"Endpoint":"http://attacker:5001","ApiKeySecretRef":"env:ServerHistorian__ApiKey"}}""")]
|
|
[InlineData(DriverTypeNames.Galaxy, """{"Gateway":{"Endpoint":"http://attacker:5001","ApiKeySecretRef":"file:appsettings.json"}}""")]
|
|
[InlineData(DriverTypeNames.OpcUaClient, """{"EndpointUrl":"opc.tcp://attacker:4840","Password":"secret:otopcua/historian/api-key"}""")]
|
|
[InlineData(DriverTypeNames.Heidenhain, """{"Access":{"InspectPassword":"secret:galaxy/main/apikey"},"Devices":[{"Host":"attacker"}]}""")]
|
|
public void Out_of_namespace_reference_never_reaches_the_probe(string driverType, string config)
|
|
{
|
|
var probe = new RecordingProbe(driverType);
|
|
var actor = Sys.ActorOf(AdminOperationsActor.Props(NewInMemoryDbFactory(), CreateTestProbe("coord").Ref, new IDriverProbe[] { probe }));
|
|
|
|
actor.Tell(new TestDriverConnect(driverType, config, 5, Guid.NewGuid()));
|
|
|
|
var reply = ExpectMsg<TestDriverConnectResult>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
|
|
reply.Ok.ShouldBeFalse();
|
|
reply.Message.ShouldNotBeNull().ShouldContain("Test Connect refused");
|
|
reply.Message.ShouldContain("lmxopcua#815");
|
|
probe.Calls.ShouldBe(0);
|
|
}
|
|
|
|
/// <summary>Positive control: a namespaced reference does reach the probe (the refusal is not blanket).</summary>
|
|
[Fact]
|
|
public void A_namespaced_reference_reaches_the_probe()
|
|
{
|
|
var probe = new RecordingProbe(DriverTypeNames.Galaxy);
|
|
var actor = Sys.ActorOf(AdminOperationsActor.Props(NewInMemoryDbFactory(), CreateTestProbe("coord").Ref, new IDriverProbe[] { probe }));
|
|
|
|
actor.Tell(new TestDriverConnect(
|
|
DriverTypeNames.Galaxy, """{"Gateway":{"Endpoint":"http://gw:5001","ApiKeySecretRef":"secret:galaxy/main/apikey"}}""", 5, Guid.NewGuid()));
|
|
|
|
ExpectMsg<TestDriverConnectResult>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken).Ok.ShouldBeTrue();
|
|
probe.Calls.ShouldBe(1);
|
|
}
|
|
|
|
private sealed class RecordingProbe(string driverType) : IDriverProbe
|
|
{
|
|
private int _calls;
|
|
|
|
public int Calls => Volatile.Read(ref _calls);
|
|
|
|
public string DriverType => driverType;
|
|
|
|
public Task<DriverProbeResult> ProbeAsync(string configJson, TimeSpan timeout, CancellationToken ct)
|
|
{
|
|
Interlocked.Increment(ref _calls);
|
|
return Task.FromResult(new DriverProbeResult(true, null, TimeSpan.Zero));
|
|
}
|
|
}
|
|
}
|