// Live user-bind identity test (Auth-007, scadaproj#40) — opt-in only. // // Start the throwaway fixture (NOT the shared rig — it needs users that can search): // cd tests/ZB.MOM.WW.Auth.Ldap.Tests/Integration/glauth-userbind && docker compose up -d // Run: // ZB_LDAP_USERBIND_IT=1 dotnet test tests/ZB.MOM.WW.Auth.Ldap.Tests \ // --filter "FullyQualifiedName~GLAuthUserBindIdentityTests" // Optional: ZB_LDAP_USERBIND_SERVER (default localhost), ZB_LDAP_USERBIND_PORT (default 3897). // // GLAuth does not implement RFC 4532 Who-am-I (it answers protocol error 2), so these tests also // prove the DN-template fallback path against a real directory. using System.Net.Sockets; using ZB.MOM.WW.Auth.Abstractions.Ldap; namespace ZB.MOM.WW.Auth.Ldap.Tests.Integration; public sealed class GLAuthUserBindIdentityTests { private static LdapOptions Options(string template, string userNameAttribute) { var server = Environment.GetEnvironmentVariable("ZB_LDAP_USERBIND_SERVER") ?? "localhost"; var port = int.TryParse(Environment.GetEnvironmentVariable("ZB_LDAP_USERBIND_PORT"), out var p) ? p : 3897; try { using var tcp = new TcpClient(); if (!tcp.ConnectAsync(server, port).Wait(TimeSpan.FromSeconds(3))) Skip.If(true, $"GLAuth fixture not reachable at {server}:{port}."); } catch (Exception ex) { Skip.If(true, $"GLAuth fixture not reachable at {server}:{port}: {ex.Message}"); } return new LdapOptions { Server = server, Port = port, Transport = LdapTransport.None, AllowInsecure = true, SearchBase = "dc=zb,dc=local", BindMode = LdapBindMode.User, UserBindTemplate = template, UserNameAttribute = userNameAttribute, GroupAttribute = "memberOf", ConnectionTimeoutMs = 3000, }; } /// /// mallory binds with her own password; (mail=mallory) finds the boss entry, which /// is in Admins. Before the fix this signed mallory in with boss's groups. /// [SkippableFact] public async Task BoundAccount_DifferentFromTheSearchedEntry_IsRefused() { Skip.IfNot(Environment.GetEnvironmentVariable("ZB_LDAP_USERBIND_IT") == "1", "Set ZB_LDAP_USERBIND_IT=1 and start Integration/glauth-userbind to run."); var svc = new LdapAuthService(Options("cn={0},ou=Users,dc=zb,dc=local", "mail")); var result = await svc.AuthenticateAsync("mallory", "password", CancellationToken.None); Assert.False(result.Succeeded, $"signed in as {result.Username} with [{string.Join(",", result.Groups)}]"); Assert.Equal(LdapAuthFailure.GroupLookupFailed, result.Failure); Assert.DoesNotContain("Admins", result.Groups); } /// Positive control: the same mechanism admits a user whose bind DN is the entry's DN. [SkippableFact] public async Task BoundAccount_SameAsTheSearchedEntry_Succeeds() { Skip.IfNot(Environment.GetEnvironmentVariable("ZB_LDAP_USERBIND_IT") == "1", "Set ZB_LDAP_USERBIND_IT=1 and start Integration/glauth-userbind to run."); // GLAuth reports search DNs as cn=,ou=,ou=users,; it also accepts that // form as a bind DN, so this template binds the exact DN the search returns. var svc = new LdapAuthService(Options("cn={0},ou=Users,ou=users,dc=zb,dc=local", "cn")); var result = await svc.AuthenticateAsync("boss", "password", CancellationToken.None); Assert.True(result.Succeeded, $"failed: {result.Failure}"); Assert.Equal("boss", result.Username); Assert.Contains("Admins", result.Groups); } }