d549a782fd
BindMode=User bound as UserBindTemplate({0}) and then returned the groups of
whatever entry (UserNameAttribute={0}) found - two directory lookups that can
name two accounts (Auth-007). Reproduced live against GLAuth: mallory bound as
herself and signed in with boss's Admins group.
After the bind the service now asks RFC 4532 Who-am-I and requires the entry
to be that account (dn: -> DN match; AD u:DOMAIN\name -> sAMAccountName match).
Without a Who-am-I answer only a DN template is provable (bind DN must equal
the entry DN); anything else fails closed as GroupLookupFailed. {0} is
RFC 4514-escaped in DN templates; validator message leads with EXAMPLE\{0}.
Auth 0.4.3 -> 0.4.4 (unpublished).
Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
86 lines
3.8 KiB
C#
86 lines
3.8 KiB
C#
// 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,
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// mallory binds with her own password; <c>(mail=mallory)</c> finds the <c>boss</c> entry, which
|
|
/// is in Admins. Before the fix this signed mallory in with boss's groups.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>Positive control: the same mechanism admits a user whose bind DN is the entry's DN.</summary>
|
|
[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=<user>,ou=<group>,ou=users,<base>; 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);
|
|
}
|
|
}
|