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; /// /// 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. /// [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.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); } /// Positive control: a namespaced reference reaches the browser. [Fact] public async Task A_namespaced_reference_reaches_the_browser() { var bespoke = new RecordingBrowser(DriverTypeNames.Galaxy); var svc = new BrowserSessionService( [bespoke], new BrowseSessionRegistry(), NullLogger.Instance, new RecordingUniversal(), null!, null!, SeededFactory()); await svc.OpenAsync(DriverTypeNames.Galaxy, """{"Gateway":{"ApiKeySecretRef":"secret:galaxy/main/apikey"}}""", TestContext.Current.CancellationToken); bespoke.Opens.ShouldBe(1); } /// lmxopcua#835: a reference naming ANOTHER instance's secret never opens a browser. [Theory] [InlineData("DRV-evil")] [InlineData(null)] public async Task A_reference_naming_another_instance_never_opens_a_browser(string? ownerId) { var bespoke = new RecordingBrowser(DriverTypeNames.Galaxy); var universal = new RecordingUniversal(); var svc = new BrowserSessionService( [bespoke], new BrowseSessionRegistry(), NullLogger.Instance, universal, null!, null!, SeededFactory()); var result = await svc.OpenAsync(DriverTypeNames.Galaxy, """{"Gateway":{"Endpoint":"http://evil:5001","ApiKeySecretRef":"secret:galaxy/DRV-victim/apikey"}}""", ownerId, TestContext.Current.CancellationToken); result.Ok.ShouldBeFalse(); result.Message.ShouldNotBeNull().ShouldContain("lmxopcua#835"); bespoke.Opens.ShouldBe(0); universal.Opens.ShouldBe(0); } /// lmxopcua#849: an env: reference named after ANOTHER instance never opens a browser either. [Theory] [InlineData("env:OTOPCUA_GALAXY_VICTIM_KEY")] [InlineData("env:GALAXY_MXGW_DRV_VICTIM_KEY")] public async Task An_env_reference_naming_another_instance_never_opens_a_browser(string reference) { var bespoke = new RecordingBrowser(DriverTypeNames.Galaxy); var svc = new BrowserSessionService( [bespoke], new BrowseSessionRegistry(), NullLogger.Instance, new RecordingUniversal(), null!, null!, SeededFactory()); var result = await svc.OpenAsync(DriverTypeNames.Galaxy, """{"Gateway":{"Endpoint":"http://evil:5001","ApiKeySecretRef":"REF"}}""".Replace("REF", reference), "DRV-evil", TestContext.Current.CancellationToken); result.Ok.ShouldBeFalse(); result.Message.ShouldNotBeNull().ShouldContain("lmxopcua#849"); bespoke.Opens.ShouldBe(0); } /// Positive control: a shared env variable that names no instance reaches the browser. [Fact] public async Task A_shared_env_variable_reaches_the_browser() { var bespoke = new RecordingBrowser(DriverTypeNames.Galaxy); var svc = new BrowserSessionService( [bespoke], new BrowseSessionRegistry(), NullLogger.Instance, new RecordingUniversal(), null!, null!, SeededFactory()); await svc.OpenAsync(DriverTypeNames.Galaxy, """{"Gateway":{"ApiKeySecretRef":"env:GALAXY_MXGW_API_KEY"}}""", TestContext.Current.CancellationToken); bespoke.Opens.ShouldBe(1); } /// Positive control: the owning driver's own id-scoped secret reaches the browser. [Fact] public async Task The_owners_own_secret_reaches_the_browser() { var bespoke = new RecordingBrowser(DriverTypeNames.Galaxy); var svc = new BrowserSessionService( [bespoke], new BrowseSessionRegistry(), NullLogger.Instance, new RecordingUniversal(), null!, null!, SeededFactory()); await svc.OpenAsync(DriverTypeNames.Galaxy, """{"Gateway":{"ApiKeySecretRef":"secret:galaxy/Victim/apikey"}}""", "DRV-victim", TestContext.Current.CancellationToken); bespoke.Opens.ShouldBe(1); } /// Without the instance list the cross-instance rule cannot be checked, so the browse fails closed. [Fact] public async Task Without_a_config_db_an_in_scope_secret_reference_fails_closed() { var bespoke = new RecordingBrowser(DriverTypeNames.Galaxy); var svc = new BrowserSessionService( [bespoke], new BrowseSessionRegistry(), NullLogger.Instance, new RecordingUniversal(), null!, null!); var result = await svc.OpenAsync(DriverTypeNames.Galaxy, """{"Gateway":{"ApiKeySecretRef":"secret:galaxy/main/apikey"}}""", TestContext.Current.CancellationToken); result.Ok.ShouldBeFalse(); result.Message.ShouldNotBeNull().ShouldContain("fail-closed"); bespoke.Opens.ShouldBe(0); } /// lmxopcua#836: an absolute OpcUaClient user-certificate path never opens a browser. [Fact] public async Task An_absolute_user_certificate_path_never_opens_a_browser() { var bespoke = new RecordingBrowser(DriverTypeNames.OpcUaClient); var svc = new BrowserSessionService( [bespoke], new BrowseSessionRegistry(), NullLogger.Instance, new RecordingUniversal(), null!, null!); var result = await svc.OpenAsync(DriverTypeNames.OpcUaClient, """{"EndpointUrl":"opc.tcp://evil:4840","AuthType":"Certificate","UserCertificatePath":"C:\\ProgramData\\x\\host.pfx"}""", TestContext.Current.CancellationToken); result.Ok.ShouldBeFalse(); result.Message.ShouldNotBeNull().ShouldContain("lmxopcua#836"); bespoke.Opens.ShouldBe(0); } private static Microsoft.EntityFrameworkCore.IDbContextFactory SeededFactory() { var name = $"browse-835-{Guid.NewGuid():N}"; using (var db = Uns.RawTreeTestDb.CreateNamed(name)) { db.DriverInstances.Add(new ZB.MOM.WW.OtOpcUa.Configuration.Entities.DriverInstance { DriverInstanceId = "DRV-victim", ClusterId = "c", Name = "Victim", DriverType = DriverTypeNames.Galaxy, DriverConfig = "{}", }); db.DriverInstances.Add(new ZB.MOM.WW.OtOpcUa.Configuration.Entities.DriverInstance { DriverInstanceId = "DRV-evil", ClusterId = "c", Name = "Evil", DriverType = DriverTypeNames.Galaxy, DriverConfig = "{}", }); db.SaveChanges(); } return Uns.RawTreeTestDb.Factory(name); } private sealed class RecordingBrowser(string driverType) : IDriverBrowser { public int Opens { get; private set; } public string DriverType => driverType; public Task 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 OpenAsync(string driverType, string configJson, CancellationToken ct) { Opens++; throw new InvalidOperationException("opened"); } } }