diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/ExternalSystems.razor b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/ExternalSystems.razor index 83d75ff1..11643ba8 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/ExternalSystems.razor +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/ExternalSystems.razor @@ -245,7 +245,8 @@ -

@dc.ConnectionString

+ @* Connection strings carry credentials — never rendered here (not even for admins). Shown only on the create/edit form. *@ +

Connection string hidden — edit to view

Max @dc.MaxRetries retries Delay @dc.RetryDelay.TotalSeconds s diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Design/ExternalSystemsConnectionStringHidingTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Design/ExternalSystemsConnectionStringHidingTests.cs new file mode 100644 index 00000000..f4ee3b0c --- /dev/null +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Design/ExternalSystemsConnectionStringHidingTests.cs @@ -0,0 +1,73 @@ +using System.Security.Claims; +using Bunit; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; +using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared; +using ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems; +using ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi; +using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories; +using ZB.MOM.WW.ScadaBridge.Security; +using ExternalSystems = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Design.ExternalSystems; + +namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Design; + +/// +/// Security regression guard: the Integration Definitions list (Database Connections +/// tab) must NEVER render a connection string — it carries credentials. Connection +/// strings are shown only on the create/edit form. This test seeds a connection whose +/// string contains a sentinel password and asserts it is absent from the rendered +/// markup of the list (so it never reaches the browser DOM, even for an admin/designer). +/// +public class ExternalSystemsConnectionStringHidingTests : BunitContext +{ + private readonly IExternalSystemRepository _repo = Substitute.For(); + private readonly IInboundApiRepository _inbound = Substitute.For(); + private readonly IDialogService _dialog = Substitute.For(); + + public ExternalSystemsConnectionStringHidingTests() + { + Services.AddSingleton(_repo); + Services.AddSingleton(_inbound); + Services.AddSingleton(_dialog); + + var claims = new[] + { + new Claim(JwtTokenService.UsernameClaimType, "tester"), + new Claim(JwtTokenService.RoleClaimType, "Designer"), + }; + var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth")); + Services.AddSingleton(new TestAuthStateProvider(user)); + Services.AddAuthorizationCore(); + AuthorizationPolicies.AddScadaBridgeAuthorization(Services); + } + + [Fact] + public void DbConnectionsTab_DoesNotRenderConnectionString() + { + const string secret = "Server=db.internal;User Id=sa;Password=SuperSecret#123;"; + + _repo.GetAllExternalSystemsAsync(Arg.Any()) + .Returns((IReadOnlyList)new List()); + _repo.GetAllDatabaseConnectionsAsync(Arg.Any()) + .Returns((IReadOnlyList)new List + { + new("AppDb", secret) { Id = 1 }, + }); + _inbound.GetAllApiMethodsAsync(Arg.Any()) + .Returns((IReadOnlyList)new List()); + + var cut = Render(); + + // Wait for the async load to complete (tabs render only once _loading == false), + // then activate the Database Connections tab so its cards render. + cut.WaitForAssertion(() => + Assert.Contains(cut.FindAll("button.nav-link"), b => b.TextContent.Contains("Database Connections"))); + cut.FindAll("button.nav-link").First(b => b.TextContent.Contains("Database Connections")).Click(); + cut.WaitForAssertion(() => Assert.Contains("AppDb", cut.Markup)); + + // The card renders (name visible) but the credential-bearing connection string must not. + Assert.DoesNotContain(secret, cut.Markup); + Assert.DoesNotContain("SuperSecret#123", cut.Markup); + } +}