diff --git a/ZB.MOM.WW.Auth/src/ZB.MOM.WW.Auth.Ldap/Internal/LdapUserName.cs b/ZB.MOM.WW.Auth/src/ZB.MOM.WW.Auth.Ldap/Internal/LdapUserName.cs new file mode 100644 index 0000000..8c500a6 --- /dev/null +++ b/ZB.MOM.WW.Auth/src/ZB.MOM.WW.Auth.Ldap/Internal/LdapUserName.cs @@ -0,0 +1,57 @@ +namespace ZB.MOM.WW.Auth.Ldap.Internal; + +/// +/// Reduces a typed login to the bare account name used by : +/// one spelling flows into the bind template, the search filter and the returned username, so audit +/// rows never carry DOMAIN\jdoe for one sign-in and jdoe for the next. +/// +internal static class LdapUserName +{ + /// + /// Trims and, when , strips one + /// leading DOMAIN\ or one trailing @suffix. Returns (with + /// empty) for blank input, for both forms at once, for a repeated + /// separator, for an empty side, or for any qualified form when + /// is . + /// + public static bool TryNormalize(string? input, bool acceptDomainQualified, out string name) + { + name = ""; + if (string.IsNullOrWhiteSpace(input)) + return false; + + var s = input.Trim(); + var slash = s.IndexOf('\\'); + var at = s.IndexOf('@'); + + if (slash >= 0 || at >= 0) + { + if (!acceptDomainQualified || (slash >= 0 && at >= 0)) + return false; + + if (slash >= 0) + { + if (slash == 0 || s.IndexOf('\\', slash + 1) >= 0) + return false; + s = s[(slash + 1)..]; + } + else + { + // Unlike the slash branch (whose extracted suffix is checked for emptiness by the + // final length check below), the extracted piece here is the *prefix* — so an empty + // domain part (a trailing "@", e.g. "jdoe@") must be rejected explicitly. + if (at == s.Length - 1 || s.IndexOf('@', at + 1) >= 0) + return false; + s = s[..at]; + } + + s = s.Trim(); + } + + if (s.Length == 0) + return false; + + name = s; + return true; + } +} diff --git a/ZB.MOM.WW.Auth/tests/ZB.MOM.WW.Auth.Ldap.Tests/LdapUserNameTests.cs b/ZB.MOM.WW.Auth/tests/ZB.MOM.WW.Auth.Ldap.Tests/LdapUserNameTests.cs new file mode 100644 index 0000000..8d0da1b --- /dev/null +++ b/ZB.MOM.WW.Auth/tests/ZB.MOM.WW.Auth.Ldap.Tests/LdapUserNameTests.cs @@ -0,0 +1,47 @@ +using ZB.MOM.WW.Auth.Ldap.Internal; + +namespace ZB.MOM.WW.Auth.Ldap.Tests; + +public class LdapUserNameTests +{ + [Theory] + [InlineData("jdoe")] + [InlineData(" jdoe ")] + [InlineData(@"NAM\jdoe")] + [InlineData("jdoe@corp.test")] + public void Accepting_ReducesToBareName(string input) + { + Assert.True(LdapUserName.TryNormalize(input, acceptDomainQualified: true, out var name)); + Assert.Equal("jdoe", name); + } + + [Theory] + [InlineData(@"NAM\jdoe")] + [InlineData("jdoe@corp.test")] + public void Strict_RefusesQualifiedNames(string input) => + Assert.False(LdapUserName.TryNormalize(input, acceptDomainQualified: false, out _)); + + [Fact] + public void Strict_AcceptsBareName() + { + Assert.True(LdapUserName.TryNormalize(" jdoe ", acceptDomainQualified: false, out var name)); + Assert.Equal("jdoe", name); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("@x")] + [InlineData(@"\jdoe")] + [InlineData("jdoe@")] + [InlineData(@"NAM\")] + [InlineData(@"a\b\c")] + [InlineData("a@b@c")] + [InlineData(@"NAM\jdoe@corp.test")] + public void Unusable_IsRefused(string? input) + { + Assert.False(LdapUserName.TryNormalize(input, acceptDomainQualified: true, out var name)); + Assert.Equal("", name); + } +}