using System.Security.Claims; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using ZB.MOM.WW.Auth.Abstractions.Ldap; using ZB.MOM.WW.Auth.Ldap; using ZB.MOM.WW.MxGateway.Server.Configuration; using ZB.MOM.WW.MxGateway.Server.Dashboard; using LibraryLdapOptions = ZB.MOM.WW.Auth.Abstractions.Ldap.LdapOptions; namespace ZB.MOM.WW.MxGateway.IntegrationTests; [Collection(LiveResourcesCollection.Name)] [Trait("Category", "LiveLdap")] public sealed class DashboardLdapLiveTests { /// Verifies that an admin user in the GwAdmin group authenticates successfully. [LiveLdapFact] public async Task AuthenticateAsync_AdminInGwAdminGroup_Succeeds() { DashboardAuthenticator authenticator = CreateAuthenticator(); DashboardAuthenticationResult result = await authenticator.AuthenticateAsync( "admin", "admin123", CancellationToken.None); Assert.True(result.Succeeded); Assert.NotNull(result.Principal); Assert.Equal("admin", result.Principal.FindFirst(ClaimTypes.NameIdentifier)?.Value); Assert.Contains(result.Principal.Claims, claim => claim.Type == DashboardAuthenticationDefaults.LdapGroupClaimType && claim.Value.Contains("GwAdmin", StringComparison.OrdinalIgnoreCase)); // IntegrationTests-023: DashboardAuthenticator builds the principal with a // ClaimTypes.Role claim resolved from the LDAP groups via the // DashboardGroupRoleMapper. The seeded GroupToRole map (GwAdmin -> Admin) // means the admin principal must carry Role=Admin alongside the raw LDAP-group // claim. A regression in the group→role mapping would fail this assertion. Assert.Contains(result.Principal.Claims, claim => claim.Type == ClaimTypes.Role && claim.Value == DashboardRoles.Admin); } /// Verifies that a readonly user without GwAdmin group fails to authenticate. [LiveLdapFact] public async Task AuthenticateAsync_ReadOnlyUserMissingGwAdminGroup_Fails() { DashboardAuthenticator authenticator = CreateAuthenticator(); DashboardAuthenticationResult result = await authenticator.AuthenticateAsync( "readonly", "readonly123", CancellationToken.None); Assert.False(result.Succeeded); Assert.Null(result.Principal); Assert.DoesNotContain("readonly123", result.FailureMessage, StringComparison.Ordinal); } /// Verifies that authentication with wrong password fails without leaking the password. [LiveLdapFact] public async Task AuthenticateAsync_AdminWithWrongPassword_FailsWithoutLeakingPassword() { // Exercises the user-bind-failure branch: the user exists and the service // account search succeeds, but the candidate bind is rejected. const string wrongPassword = "definitely-not-the-admin-password"; DashboardAuthenticator authenticator = CreateAuthenticator(); DashboardAuthenticationResult result = await authenticator.AuthenticateAsync( "admin", wrongPassword, CancellationToken.None); Assert.False(result.Succeeded); Assert.Null(result.Principal); Assert.DoesNotContain(wrongPassword, result.FailureMessage, StringComparison.Ordinal); } /// Verifies that authentication with unknown username fails. [LiveLdapFact] public async Task AuthenticateAsync_UnknownUsername_Fails() { // Exercises the user-not-found branch: the service-account search returns no // entry, so no candidate bind is attempted. DashboardAuthenticator authenticator = CreateAuthenticator(); DashboardAuthenticationResult result = await authenticator.AuthenticateAsync( "no-such-user-9f3c1", "irrelevant-password", CancellationToken.None); Assert.False(result.Succeeded); Assert.Null(result.Principal); } /// Verifies that authentication fails gracefully when the server is unreachable. [LiveLdapFact] public async Task AuthenticateAsync_ServerUnreachable_FailsWithoutThrowing() { // Exercises the connect-failure path: a closed loopback port produces a // connection error that the shared LdapAuthService must absorb into a Fail // result rather than propagating an exception to the dashboard. DashboardAuthenticator authenticator = CreateAuthenticator(LibraryOptions() with { // 1 is a reserved port number that no LDAP server listens on. Port = 1, }); DashboardAuthenticationResult result = await authenticator.AuthenticateAsync( "admin", "admin123", CancellationToken.None); Assert.False(result.Succeeded); Assert.Null(result.Principal); } private static DashboardAuthenticator CreateAuthenticator() => CreateAuthenticator(LibraryOptions()); private static DashboardAuthenticator CreateAuthenticator(LibraryLdapOptions ldapOptions) { GatewayOptions gatewayOptions = new() { Dashboard = new DashboardOptions { GroupToRole = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["GwAdmin"] = DashboardRoles.Admin, }, }, }; return new DashboardAuthenticator( new LdapAuthService(ldapOptions), new DashboardGroupRoleMapper(Options.Create(gatewayOptions)), NullLogger.Instance); } /// /// Builds the shared library from the gateway's /// default LDAP settings so the live tests exercise the same seeded directory the /// gateway connects to (localhost:3893, plaintext, with AllowInsecure for dev). /// private static LibraryLdapOptions LibraryOptions() { ZB.MOM.WW.MxGateway.Server.Configuration.LdapOptions gateway = new(); return new LibraryLdapOptions { Enabled = gateway.Enabled, Server = gateway.Server, Port = gateway.Port, Transport = gateway.Transport, AllowInsecure = gateway.AllowInsecure, SearchBase = gateway.SearchBase, ServiceAccountDn = gateway.ServiceAccountDn, ServiceAccountPassword = gateway.ServiceAccountPassword, UserNameAttribute = gateway.UserNameAttribute, DisplayNameAttribute = gateway.DisplayNameAttribute, GroupAttribute = gateway.GroupAttribute, }; } }