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>
79 lines
3.3 KiB
C#
79 lines
3.3 KiB
C#
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.AdminUI.Browsing;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Browsing;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Browsing;
|
|
|
|
/// <summary>
|
|
/// lmxopcua#815: the AdminUI live browse (Galaxy / OpcUaClient bespoke browsers, and the Discover-backed universal
|
|
/// browser) resolves the config's credential references and sends them to the author-chosen endpoint — an
|
|
/// out-of-namespace reference is refused before any browser is opened.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class BrowserSessionCredentialScopeTests
|
|
{
|
|
[Theory]
|
|
[InlineData(DriverTypeNames.Galaxy, """{"Gateway":{"Endpoint":"http://evil:5001","ApiKeySecretRef":"env:ConnectionStrings__ConfigDb"}}""")]
|
|
[InlineData(DriverTypeNames.OpcUaClient, """{"EndpointUrl":"opc.tcp://evil:4840","Password":"secret:otopcua/historian/api-key"}""")]
|
|
[InlineData(DriverTypeNames.Heidenhain, """{"Access":{"InspectPassword":"secret:galaxy/main/apikey"}}""")]
|
|
public async Task Out_of_namespace_reference_never_opens_a_browser(string driverType, string config)
|
|
{
|
|
var bespoke = new RecordingBrowser(DriverTypeNames.Galaxy);
|
|
var bespoke2 = new RecordingBrowser(DriverTypeNames.OpcUaClient);
|
|
var universal = new RecordingUniversal();
|
|
var svc = new BrowserSessionService(
|
|
[bespoke, bespoke2], new BrowseSessionRegistry(), NullLogger<BrowserSessionService>.Instance, universal, null!, null!);
|
|
|
|
var result = await svc.OpenAsync(driverType, config, TestContext.Current.CancellationToken);
|
|
|
|
result.Ok.ShouldBeFalse();
|
|
result.Message.ShouldNotBeNull().ShouldContain("lmxopcua#815");
|
|
bespoke.Opens.ShouldBe(0);
|
|
bespoke2.Opens.ShouldBe(0);
|
|
universal.Opens.ShouldBe(0);
|
|
}
|
|
|
|
/// <summary>Positive control: a namespaced reference reaches the browser.</summary>
|
|
[Fact]
|
|
public async Task A_namespaced_reference_reaches_the_browser()
|
|
{
|
|
var bespoke = new RecordingBrowser(DriverTypeNames.Galaxy);
|
|
var svc = new BrowserSessionService(
|
|
[bespoke], new BrowseSessionRegistry(), NullLogger<BrowserSessionService>.Instance, new RecordingUniversal(), null!, null!);
|
|
|
|
await svc.OpenAsync(DriverTypeNames.Galaxy, """{"Gateway":{"ApiKeySecretRef":"secret:galaxy/main/apikey"}}""",
|
|
TestContext.Current.CancellationToken);
|
|
|
|
bespoke.Opens.ShouldBe(1);
|
|
}
|
|
|
|
private sealed class RecordingBrowser(string driverType) : IDriverBrowser
|
|
{
|
|
public int Opens { get; private set; }
|
|
|
|
public string DriverType => driverType;
|
|
|
|
public Task<IBrowseSession> OpenAsync(string configJson, CancellationToken cancellationToken)
|
|
{
|
|
Opens++;
|
|
throw new InvalidOperationException("opened");
|
|
}
|
|
}
|
|
|
|
private sealed class RecordingUniversal : IUniversalDriverBrowser
|
|
{
|
|
public int Opens { get; private set; }
|
|
|
|
public bool CanBrowse(string driverType, string configJson) => true;
|
|
|
|
public Task<IBrowseSession> OpenAsync(string driverType, string configJson, CancellationToken ct)
|
|
{
|
|
Opens++;
|
|
throw new InvalidOperationException("opened");
|
|
}
|
|
}
|
|
}
|