b8d3fa2ba1
Auth-007's sAMAccountName match assumed the search base covers one AD domain; across a forest (Global Catalog) it could accept another domain's same-named account and return its groups. For a u:DOMAIN\name Who-am-I answer the entry must now also be that domain's account: AD's msDS-PrincipalName (objectSid-derived DOMAIN\name, read by a base search through the new ILdapConnection.Read) must equal the answer where offered; otherwise the name must have exactly one holder under SearchBase and it must be the entry found. Three forest cases fail pre-fix; single-domain and sAMAccountName-search compat cases pass both ways. README notes this and Auth-009. (scadaproj#50) Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
479 lines
19 KiB
C#
479 lines
19 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 const string JaneDn = "CN=Jane Doe,OU=u,DC=x";
|
|
|
|
private static FakeLdapConnection Jane() => new FakeLdapConnection()
|
|
.WithWhoAmI("dn:" + JaneDn)
|
|
.WithUserEntry(JaneDn, 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", "whoami", "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().WithWhoAmI("dn:" + JaneDn).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().WithWhoAmI("dn:cn=dup1,dc=x").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().WithWhoAmI("dn:CN=Jane,DC=x").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().WithWhoAmI(@"u:NAM\jdoe").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);
|
|
}
|
|
|
|
// ---- Auth-007 (scadaproj#40): the entry whose groups are returned must be the account that bound ----
|
|
|
|
/// <summary>
|
|
/// The review's scenario: a UPN bind for <c>jsmith</c> is resolved by AD to Mallory (explicit UPN
|
|
/// <c>jsmith@corp.test</c>, sAMAccountName <c>jsmith2</c>), while <c>(sAMAccountName=jsmith)</c>
|
|
/// finds the admin. Before the fix this returned the admin's groups to Mallory.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task UpnBind_ResolvedToAnotherAccount_DoesNotReturnTheSearchedEntrysGroups()
|
|
{
|
|
var fake = new FakeLdapConnection()
|
|
.WithWhoAmI(@"u:CORP\jsmith2")
|
|
.WithUserEntry("CN=Admin Smith,OU=u,DC=x", new[] { "CN=UG_Admins,OU=g,DC=x" }, "Admin Smith",
|
|
new Dictionary<string, string[]> { ["sAMAccountName"] = new[] { "jsmith" } });
|
|
var factory = new SequenceLdapConnectionFactory(fake);
|
|
|
|
var result = await Login(new LdapAuthService(Opts(), factory), "jsmith", "mallorys-own-password");
|
|
|
|
Assert.False(result.Succeeded);
|
|
Assert.Equal(LdapAuthFailure.GroupLookupFailed, result.Failure);
|
|
Assert.Empty(result.Groups);
|
|
Assert.Equal(new[] { "jsmith@corp.test" }, fake.BoundDns);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task WhoAmIAccountName_MatchingTheEntry_Succeeds_AndRequestsSamAccountName()
|
|
{
|
|
var fake = new FakeLdapConnection()
|
|
.WithWhoAmI(@"u:CORP\JSmith")
|
|
.WithUserEntry("CN=John Smith,OU=u,DC=x", new[] { "CN=UG_A,OU=g,DC=x" }, "John Smith",
|
|
new Dictionary<string, string[]> { ["sAMAccountName"] = new[] { "jsmith" } });
|
|
var factory = new SequenceLdapConnectionFactory(fake);
|
|
var svc = new LdapAuthService(Opts() with { UserNameAttribute = "cn" }, factory);
|
|
|
|
var result = await Login(svc, "John Smith");
|
|
|
|
Assert.True(result.Succeeded);
|
|
Assert.Contains("sAMAccountName", fake.SearchAttributes!);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task WhoAmIAccountName_WithoutDomain_IsAccepted()
|
|
{
|
|
var fake = new FakeLdapConnection()
|
|
.WithWhoAmI("u:jdoe")
|
|
.WithUserEntry(JaneDn, new[] { "CN=UG_A,OU=g,DC=x" }, "Jane Doe",
|
|
new Dictionary<string, string[]> { ["sAMAccountName"] = new[] { "jdoe" } });
|
|
|
|
var result = await Login(new LdapAuthService(Opts(), new SequenceLdapConnectionFactory(fake)), "jdoe");
|
|
|
|
Assert.True(result.Succeeded);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task WhoAmIAccountName_EntryWithoutSamAccountName_FailsClosed()
|
|
{
|
|
var fake = new FakeLdapConnection()
|
|
.WithWhoAmI(@"u:CORP\jdoe")
|
|
.WithUserEntry(JaneDn, new[] { "CN=UG_A,OU=g,DC=x" }, "Jane Doe");
|
|
|
|
var result = await Login(new LdapAuthService(Opts(), new SequenceLdapConnectionFactory(fake)), "jdoe");
|
|
|
|
Assert.Equal(LdapAuthFailure.GroupLookupFailed, result.Failure);
|
|
}
|
|
|
|
// ---- scadaproj#50: an AD forest — the same sAMAccountName in two domains ----------------------
|
|
|
|
// A Global Catalog search base spans every domain in the forest. CORP\jsmith binds; the search
|
|
// (here on mail, so the filter is not sAMAccountName) finds OTHER\jsmith, whose sAMAccountName is
|
|
// also "jsmith". 0.4.4 compared sAMAccountName alone and returned OTHER\jsmith's groups.
|
|
private const string OtherDn = "CN=J Smith,OU=u,DC=other,DC=forest";
|
|
private const string CorpDn = "CN=John Smith,OU=u,DC=corp,DC=forest";
|
|
|
|
private static FakeLdapConnection ForestSearchFinding(string dn) => new FakeLdapConnection()
|
|
.WithWhoAmI(@"u:CORP\jsmith")
|
|
.WithUserEntry(dn, new[] { "CN=UG_Admins,OU=g,DC=other,DC=forest" }, "J Smith",
|
|
new Dictionary<string, string[]> { ["sAMAccountName"] = new[] { "jsmith" }, ["mail"] = new[] { "jsmith" } });
|
|
|
|
private static LdapOptions ForestOpts() => Opts() with { UserNameAttribute = "mail", SearchBase = "DC=forest" };
|
|
|
|
[Fact]
|
|
public async Task Forest_PrincipalNameInAnotherDomain_FailsClosed()
|
|
{
|
|
var fake = ForestSearchFinding(OtherDn).WithPrincipalName(@"OTHER\jsmith");
|
|
|
|
var result = await Login(new LdapAuthService(ForestOpts(), new SequenceLdapConnectionFactory(fake)), "jsmith");
|
|
|
|
Assert.Equal(LdapAuthFailure.GroupLookupFailed, result.Failure);
|
|
Assert.Empty(result.Groups);
|
|
Assert.Contains("read:" + OtherDn, fake.Calls);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Forest_PrincipalNameInTheBoundDomain_Succeeds()
|
|
{
|
|
var fake = ForestSearchFinding(CorpDn).WithPrincipalName(@"corp\JSMITH");
|
|
|
|
var result = await Login(new LdapAuthService(ForestOpts(), new SequenceLdapConnectionFactory(fake)), "jsmith");
|
|
|
|
Assert.True(result.Succeeded);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Forest_NoPrincipalName_AndTheNameIsHeldInTwoDomains_FailsClosed()
|
|
{
|
|
// The directory offers no msDS-PrincipalName, so which jsmith bound cannot be proven; with two
|
|
// holders of the name in scope the sign-in is refused rather than guessed.
|
|
var fake = ForestSearchFinding(OtherDn)
|
|
.WithSearchResult("(sAMAccountName=jsmith)", (CorpDn, "jsmith"), (OtherDn, "jsmith"));
|
|
|
|
var result = await Login(new LdapAuthService(ForestOpts(), new SequenceLdapConnectionFactory(fake)), "jsmith");
|
|
|
|
Assert.Equal(LdapAuthFailure.GroupLookupFailed, result.Failure);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Forest_NoPrincipalName_AndTheReadFails_StillFallsBackToTheUniquenessCheck()
|
|
{
|
|
var fake = ForestSearchFinding(OtherDn).ThrowOnRead()
|
|
.WithSearchResult("(sAMAccountName=jsmith)", (CorpDn, "jsmith"), (OtherDn, "jsmith"));
|
|
|
|
var result = await Login(new LdapAuthService(ForestOpts(), new SequenceLdapConnectionFactory(fake)), "jsmith");
|
|
|
|
Assert.Equal(LdapAuthFailure.GroupLookupFailed, result.Failure);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SingleDomain_NoPrincipalName_UniqueName_Succeeds()
|
|
{
|
|
// Compatibility: a single-domain directory without msDS-PrincipalName still signs in — the
|
|
// name has exactly one holder in scope, and it is the entry found.
|
|
var fake = ForestSearchFinding(CorpDn).WithSearchResult("(sAMAccountName=jsmith)", (CorpDn, "jsmith"));
|
|
|
|
var result = await Login(new LdapAuthService(ForestOpts(), new SequenceLdapConnectionFactory(fake)), "jsmith");
|
|
|
|
Assert.True(result.Succeeded);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SamAccountNameSearch_ForTheBoundName_NeedsNoSecondSearch()
|
|
{
|
|
// When the user search itself was (sAMAccountName=<the bound name>) and found exactly one
|
|
// entry, the name is already proven unique in scope.
|
|
var fake = new FakeLdapConnection()
|
|
.WithWhoAmI(@"u:CORP\jdoe")
|
|
.WithUserEntry(JaneDn, new[] { "CN=UG_A,OU=g,DC=x" }, "Jane Doe",
|
|
new Dictionary<string, string[]> { ["sAMAccountName"] = new[] { "jdoe" } });
|
|
|
|
var result = await Login(new LdapAuthService(Opts(), new SequenceLdapConnectionFactory(fake)), "jdoe");
|
|
|
|
Assert.True(result.Succeeded);
|
|
Assert.Single(fake.Calls, c => c.StartsWith("search:", StringComparison.Ordinal));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task WhoAmIDn_NamingAnotherEntry_FailsClosed()
|
|
{
|
|
var fake = new FakeLdapConnection()
|
|
.WithWhoAmI("dn:CN=Mallory,OU=u,DC=x")
|
|
.WithUserEntry("CN=Admin Smith,OU=u,DC=x", new[] { "CN=UG_Admins,OU=g,DC=x" }, "Admin Smith");
|
|
|
|
var result = await Login(new LdapAuthService(Opts(), new SequenceLdapConnectionFactory(fake)), "jsmith");
|
|
|
|
Assert.Equal(LdapAuthFailure.GroupLookupFailed, result.Failure);
|
|
Assert.Empty(result.Groups);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task WhoAmIDn_IsComparedAsADn_NotAsAString()
|
|
{
|
|
var fake = new FakeLdapConnection()
|
|
.WithWhoAmI("dn:cn=jane doe, ou=U, dc=X")
|
|
.WithUserEntry(JaneDn, new[] { "CN=UG_A,OU=g,DC=x" }, "Jane Doe");
|
|
|
|
var result = await Login(new LdapAuthService(Opts(), new SequenceLdapConnectionFactory(fake)), "jdoe");
|
|
|
|
Assert.True(result.Succeeded);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(null)] // server does not support Who-am-I (GLAuth: protocol error 2)
|
|
[InlineData("")] // anonymous / empty answer
|
|
[InlineData("x:garbage")]
|
|
public async Task UpnTemplate_WithoutAWhoAmIAnswer_FailsClosed_WithoutSearching(string? whoAmI)
|
|
{
|
|
var fake = new FakeLdapConnection()
|
|
.WithUserEntry(JaneDn, new[] { "CN=UG_A,OU=g,DC=x" }, "Jane Doe");
|
|
if (whoAmI is not null)
|
|
fake.WithWhoAmI(whoAmI);
|
|
|
|
var result = await Login(new LdapAuthService(Opts(), new SequenceLdapConnectionFactory(fake)), "jdoe");
|
|
|
|
Assert.Equal(LdapAuthFailure.GroupLookupFailed, result.Failure);
|
|
Assert.DoesNotContain(fake.Calls, c => c.StartsWith("search:", StringComparison.Ordinal));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DownLevelTemplate_WithoutAWhoAmIAnswer_FailsClosed()
|
|
{
|
|
var fake = new FakeLdapConnection()
|
|
.WithUserEntry(JaneDn, new[] { "CN=UG_A,OU=g,DC=x" }, "Jane Doe");
|
|
var svc = new LdapAuthService(Opts() with { UserBindTemplate = @"CORP\{0}" }, new SequenceLdapConnectionFactory(fake));
|
|
|
|
var result = await Login(svc, "jdoe");
|
|
|
|
Assert.Equal(LdapAuthFailure.GroupLookupFailed, result.Failure);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DnTemplate_WithoutWhoAmI_AcceptsTheEntryAtTheBoundDn()
|
|
{
|
|
var fake = new FakeLdapConnection()
|
|
.WithUserEntry("cn=jdoe,ou=Users,dc=x", new[] { "cn=UG_A,ou=g,dc=x" }, "Jane Doe");
|
|
var svc = new LdapAuthService(
|
|
Opts() with { UserBindTemplate = "cn={0},ou=users,dc=x", UserNameAttribute = "cn" },
|
|
new SequenceLdapConnectionFactory(fake));
|
|
|
|
var result = await Login(svc, "jdoe");
|
|
|
|
Assert.True(result.Succeeded);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DnTemplate_WithoutWhoAmI_RefusesAnEntryAtAnotherDn()
|
|
{
|
|
// Mallory binds as cn=mallory with her own password; (mail=mallory) finds the admin.
|
|
var fake = new FakeLdapConnection()
|
|
.WithUserEntry("cn=admin,ou=Users,dc=x", new[] { "cn=UG_Admins,ou=g,dc=x" }, "Admin");
|
|
var svc = new LdapAuthService(
|
|
Opts() with { UserBindTemplate = "cn={0},ou=users,dc=x", UserNameAttribute = "mail" },
|
|
new SequenceLdapConnectionFactory(fake));
|
|
|
|
var result = await Login(svc, "mallory");
|
|
|
|
Assert.Equal(LdapAuthFailure.GroupLookupFailed, result.Failure);
|
|
Assert.Equal(new[] { "cn=mallory,ou=users,dc=x" }, fake.BoundDns);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DnTemplate_EscapesTheName_SoItCannotRewriteTheBindDn()
|
|
{
|
|
var fake = new FakeLdapConnection()
|
|
.WithUserEntry("cn=admin,ou=Admins,dc=x", new[] { "cn=UG_Admins,ou=g,dc=x" }, "Admin");
|
|
var svc = new LdapAuthService(
|
|
Opts() with { UserBindTemplate = "cn={0},ou=users,dc=x", UserNameAttribute = "cn" },
|
|
new SequenceLdapConnectionFactory(fake));
|
|
|
|
var result = await Login(svc, "admin,ou=Admins");
|
|
|
|
Assert.Equal(new[] { @"cn=admin\,ou=Admins,ou=users,dc=x" }, fake.BoundDns);
|
|
Assert.False(result.Succeeded);
|
|
}
|
|
}
|