e3d146aa21
- Request UserNameAttribute in the search (de-duplicated, case-insensitive) and return its first value as LdapAuthResult.Username, falling back to the typed name (I2). - The empty-password guard now applies in ServiceAccount mode too: AD treats an empty-password simple bind as an unauthenticated bind and reports success (I3). - Restore the dropped explanatory comments in CompleteWithServiceAccount / SearchUser.
223 lines
8.3 KiB
C#
223 lines
8.3 KiB
C#
using Novell.Directory.Ldap;
|
|
using ZB.MOM.WW.Auth.Abstractions.Ldap;
|
|
|
|
namespace ZB.MOM.WW.Auth.Ldap.Tests;
|
|
|
|
/// <summary>
|
|
/// <see cref="LdapBindMode.User"/>: bind as the signing-in user, then read their own entry on the
|
|
/// same connection. The service account in <see cref="Opts"/> is deliberately populated so the
|
|
/// tests prove it is never used.
|
|
/// </summary>
|
|
public class LdapAuthServiceUserBindTests
|
|
{
|
|
private static LdapOptions Opts(params string[] fallbacks) => new()
|
|
{
|
|
Enabled = true,
|
|
Server = "dc1",
|
|
Port = 636,
|
|
Transport = LdapTransport.Ldaps,
|
|
SearchBase = "dc=x",
|
|
UserNameAttribute = "sAMAccountName",
|
|
DisplayNameAttribute = "displayName",
|
|
GroupAttribute = "memberOf",
|
|
BindMode = LdapBindMode.User,
|
|
UserBindTemplate = "{0}@corp.test",
|
|
ServiceAccountDn = "cn=svc,dc=x",
|
|
ServiceAccountPassword = "svcpw",
|
|
FallbackServers = fallbacks,
|
|
};
|
|
|
|
private static FakeLdapConnection Jane() => new FakeLdapConnection()
|
|
.WithUserEntry("CN=Jane Doe,OU=u,DC=x", new[] { "CN=UG_A,OU=g,DC=x", "CN=UG_B,OU=g,DC=x" }, "Jane Doe");
|
|
|
|
private static Task<LdapAuthResult> Login(LdapAuthService svc, string user, string pw = "pw") =>
|
|
svc.AuthenticateAsync(user, pw, CancellationToken.None);
|
|
|
|
[Fact]
|
|
public async Task Success_BindsAsUser_ThenSearchesOnTheSameConnection()
|
|
{
|
|
var factory = new SequenceLdapConnectionFactory(Jane());
|
|
var result = await Login(new LdapAuthService(Opts(), factory), "jdoe");
|
|
|
|
Assert.True(result.Succeeded);
|
|
Assert.Equal("jdoe", result.Username);
|
|
Assert.Equal("Jane Doe", result.DisplayName);
|
|
Assert.Equal(new[] { "UG_A", "UG_B" }, result.Groups);
|
|
var conn = Assert.Single(factory.Created);
|
|
Assert.Equal(new[] { "connect", "bind:jdoe@corp.test", "search:(sAMAccountName=jdoe)" }, conn.Calls);
|
|
Assert.Equal(new[] { "jdoe@corp.test" }, conn.BoundDns);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(@"NAM\jdoe")]
|
|
[InlineData("jdoe@corp.test")]
|
|
[InlineData(" jdoe ")]
|
|
public async Task QualifiedNames_BindAsTheTemplate_AndReturnTheBareName(string typed)
|
|
{
|
|
var factory = new SequenceLdapConnectionFactory(Jane());
|
|
var result = await Login(new LdapAuthService(Opts(), factory), typed);
|
|
|
|
Assert.True(result.Succeeded);
|
|
Assert.Equal("jdoe", result.Username);
|
|
Assert.Equal(new[] { "jdoe@corp.test" }, factory.Created[0].BoundDns);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EmptyPassword_IsRefused_WithoutContactingTheDirectory()
|
|
{
|
|
var factory = new SequenceLdapConnectionFactory(Jane());
|
|
var result = await Login(new LdapAuthService(Opts(), factory), "jdoe", "");
|
|
|
|
Assert.Equal(LdapAuthFailure.BadCredentials, result.Failure);
|
|
Assert.Empty(factory.Created);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("@x")]
|
|
[InlineData(@"a\b\c")]
|
|
[InlineData("a@b@c")]
|
|
public async Task UnusableNames_AreRefused_WithoutContactingTheDirectory(string typed)
|
|
{
|
|
var factory = new SequenceLdapConnectionFactory(Jane());
|
|
var result = await Login(new LdapAuthService(Opts(), factory), typed);
|
|
|
|
Assert.Equal(LdapAuthFailure.BadCredentials, result.Failure);
|
|
Assert.Empty(factory.Created);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task StrictMode_RefusesQualifiedNames_WithoutContactingTheDirectory()
|
|
{
|
|
var factory = new SequenceLdapConnectionFactory(Jane());
|
|
var svc = new LdapAuthService(Opts() with { AcceptDomainQualifiedNames = false }, factory);
|
|
|
|
var result = await Login(svc, @"NAM\jdoe");
|
|
|
|
Assert.Equal(LdapAuthFailure.BadCredentials, result.Failure);
|
|
Assert.Empty(factory.Created);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task WrongPassword_IsFinal_AndNeverAsksTheFallback()
|
|
{
|
|
var factory = new SequenceLdapConnectionFactory(
|
|
Jane().ThrowOnFirstBind(LdapException.InvalidCredentials), Jane());
|
|
var result = await Login(new LdapAuthService(Opts("dc2"), factory), "jdoe", "wrong");
|
|
|
|
Assert.Equal(LdapAuthFailure.BadCredentials, result.Failure);
|
|
Assert.Single(factory.Created);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task NonCredentialBindError_FailsOver()
|
|
{
|
|
var factory = new SequenceLdapConnectionFactory(
|
|
Jane().ThrowOnFirstBind(LdapException.Unavailable), Jane());
|
|
var result = await Login(new LdapAuthService(Opts("dc2"), factory), "jdoe");
|
|
|
|
Assert.True(result.Succeeded);
|
|
Assert.Equal(2, factory.Created.Count);
|
|
Assert.Equal("dc2", factory.Created[1].ConnectArgs!.Value.Host);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ConnectFailure_FailsOver()
|
|
{
|
|
var factory = new SequenceLdapConnectionFactory(new FakeLdapConnection().ThrowOnConnect(), Jane());
|
|
var result = await Login(new LdapAuthService(Opts("dc2"), factory), "jdoe");
|
|
|
|
Assert.True(result.Succeeded);
|
|
Assert.Equal(2, factory.Created.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SearchFailure_FailsOver()
|
|
{
|
|
var factory = new SequenceLdapConnectionFactory(Jane().ThrowOnSearch(), Jane());
|
|
var result = await Login(new LdapAuthService(Opts("dc2"), factory), "jdoe");
|
|
|
|
Assert.True(result.Succeeded);
|
|
Assert.Equal(2, factory.Created.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EveryEndpointDown_IsSystemSide()
|
|
{
|
|
var factory = new SequenceLdapConnectionFactory(
|
|
new FakeLdapConnection().ThrowOnConnect(), new FakeLdapConnection().ThrowOnConnect());
|
|
var result = await Login(new LdapAuthService(Opts("dc2"), factory), "jdoe");
|
|
|
|
Assert.Equal(LdapAuthFailure.ServiceAccountBindFailed, result.Failure);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task NoEntryAfterAGoodBind_IsGroupLookupFailed()
|
|
{
|
|
var factory = new SequenceLdapConnectionFactory(new FakeLdapConnection().WithNoMatch());
|
|
var result = await Login(new LdapAuthService(Opts("dc2"), factory), "jdoe");
|
|
|
|
Assert.Equal(LdapAuthFailure.GroupLookupFailed, result.Failure);
|
|
Assert.Single(factory.Created);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TwoEntriesAfterAGoodBind_IsGroupLookupFailed()
|
|
{
|
|
var factory = new SequenceLdapConnectionFactory(new FakeLdapConnection().WithDuplicateMatch());
|
|
var result = await Login(new LdapAuthService(Opts(), factory), "jdoe");
|
|
|
|
Assert.Equal(LdapAuthFailure.GroupLookupFailed, result.Failure);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task NoGroups_IsGroupLookupFailed()
|
|
{
|
|
var factory = new SequenceLdapConnectionFactory(
|
|
new FakeLdapConnection().WithUserEntry("CN=Jane,DC=x", Array.Empty<string>(), "Jane"));
|
|
var result = await Login(new LdapAuthService(Opts(), factory), "jdoe");
|
|
|
|
Assert.Equal(LdapAuthFailure.GroupLookupFailed, result.Failure);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Username_IsCanonicalisedFromTheEntry_NotTheTypedCase()
|
|
{
|
|
var factory = new SequenceLdapConnectionFactory(new FakeLdapConnection().WithUserEntry(
|
|
"CN=Jane Doe,OU=u,DC=x", new[] { "CN=UG_A,OU=g,DC=x" }, "Jane Doe",
|
|
new Dictionary<string, string[]> { ["sAMAccountName"] = new[] { "jdoe" } }));
|
|
var result = await Login(new LdapAuthService(Opts(), factory), @"NAM\JDOE");
|
|
|
|
Assert.True(result.Succeeded);
|
|
Assert.Equal("jdoe", result.Username);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Username_FallsBackToTheTypedName_WhenTheEntryLacksTheAttribute()
|
|
{
|
|
var factory = new SequenceLdapConnectionFactory(Jane());
|
|
var result = await Login(new LdapAuthService(Opts(), factory), "JDOE");
|
|
|
|
Assert.True(result.Succeeded);
|
|
Assert.Equal("JDOE", result.Username);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Search_RequestsTheUserNameAttribute()
|
|
{
|
|
var factory = new SequenceLdapConnectionFactory(Jane());
|
|
await Login(new LdapAuthService(Opts(), factory), "jdoe");
|
|
|
|
Assert.Equal(new[] { "displayName", "memberOf", "sAMAccountName" }, factory.Created[0].SearchAttributes);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Search_DeDuplicatesTheUserNameAttribute_CaseInsensitively()
|
|
{
|
|
var factory = new SequenceLdapConnectionFactory(Jane());
|
|
var svc = new LdapAuthService(Opts() with { DisplayNameAttribute = "SAMACCOUNTNAME" }, factory);
|
|
await Login(svc, "jdoe");
|
|
|
|
Assert.Equal(new[] { "SAMACCOUNTNAME", "memberOf" }, factory.Created[0].SearchAttributes);
|
|
}
|
|
}
|