using BoardApp.Config; using Novell.Directory.Ldap; namespace BoardApp.Services; public sealed class LdapAuthService { private readonly BoardConfigLoader _cfg; public LdapAuthService(BoardConfigLoader cfg) { _cfg = cfg; } public enum LdapVerifyFailReason { InvalidCredentials, UserNotFound, ConnectionError, NotInAllowedGroup } public sealed record LdapVerifyResult(bool Ok, LdapVerifyFailReason? Reason); private static string EscapeLdapFilterValue(string value) { // RFC4515 subset used by Node implementation. return (value ?? "").Replace(@"\", @"\5c") .Replace("*", @"\2a") .Replace("(", @"\28") .Replace(")", @"\29") .Replace("\0", @"\00"); } private static LdapConnection CreateConnection(LdapConfig cfg) { var uri = new Uri(cfg.Url); var conn = new LdapConnection { SecureSocketLayer = uri.Scheme.Equals("ldaps", StringComparison.OrdinalIgnoreCase) }; conn.Connect(uri.Host, uri.Port > 0 ? uri.Port : (conn.SecureSocketLayer ? 636 : 389)); if (conn.SecureSocketLayer && cfg.TlsOptions?.RejectUnauthorized == false) { conn.UserDefinedServerCertValidationDelegate += (_, _, _, _) => true; } return conn; } private static string? SearchUserDn(LdapConnection conn, string searchBase, string filter) { var lc = conn.Search( searchBase, LdapConnection.ScopeSub, filter, new[] { "dn" }, false ); while (lc.HasMore()) { try { var entry = lc.Next(); return entry.Dn; } catch (LdapException) { return null; } } return null; } private static string[] GetAllowedGroupDns(LdapConfig cfg) { var list = new List(); if (!string.IsNullOrWhiteSpace(cfg.AllowedGroupDN)) { list.Add(cfg.AllowedGroupDN.Trim()); } if (cfg.AllowedGroupDNs is not null) { list.AddRange(cfg.AllowedGroupDNs.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim())); } return list.Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } private static bool IsMemberOfAllowedGroups(LdapConnection conn, string userDn, string searchBase, string[] allowedGroupDns) { if (allowedGroupDns.Length == 0) return true; // Read memberOf from the user entry (fast path). var filter = "(distinguishedName=" + EscapeLdapFilterValue(userDn) + ")"; var res = conn.Search(searchBase, LdapConnection.ScopeSub, filter, new[] { "memberOf" }, false); while (res.HasMore()) { try { var entry = res.Next(); var memberOf = entry.GetAttribute("memberOf")?.StringValueArray ?? Array.Empty(); foreach (var g in memberOf) { if (allowedGroupDns.Any(agd => string.Equals(agd, g, StringComparison.OrdinalIgnoreCase))) { return true; } } return false; } catch (LdapException) { return false; } } // If user entry not found or no memberOf, treat as not allowed when restriction exists. return false; } public async Task VerifyCredentials(string username, string password) { var user = (username ?? "").Trim(); if (user.Length == 0 || string.IsNullOrEmpty(password)) { return new LdapVerifyResult(false, LdapVerifyFailReason.InvalidCredentials); } var cfg = _cfg.LoadLdapConfig(); var filter = (cfg.SearchFilter ?? "").Replace("{{username}}", EscapeLdapFilterValue(user)); var allowedGroupDns = GetAllowedGroupDns(cfg); return await Task.Run(() => { try { using var searchConn = CreateConnection(cfg); searchConn.Bind(cfg.BindDN, cfg.BindPassword); var userDn = SearchUserDn(searchConn, cfg.SearchBase, filter); if (string.IsNullOrWhiteSpace(userDn)) { return new LdapVerifyResult(false, LdapVerifyFailReason.UserNotFound); } if (!IsMemberOfAllowedGroups(searchConn, userDn, cfg.SearchBase, allowedGroupDns)) { return new LdapVerifyResult(false, LdapVerifyFailReason.NotInAllowedGroup); } using var verifyConn = CreateConnection(cfg); try { verifyConn.Bind(userDn, password); return new LdapVerifyResult(true, null); } catch { return new LdapVerifyResult(false, LdapVerifyFailReason.InvalidCredentials); } } catch { return new LdapVerifyResult(false, LdapVerifyFailReason.ConnectionError); } }); } /// /// 用于 Windows 集成登录:不验证密码,只校验“用户是否存在 + 是否属于允许组(若配置)”。 /// public async Task VerifyUserForIntegratedLogin(string username) { var user = (username ?? "").Trim(); if (user.Length == 0) { return new LdapVerifyResult(false, LdapVerifyFailReason.InvalidCredentials); } var cfg = _cfg.LoadLdapConfig(); var filter = (cfg.SearchFilter ?? "").Replace("{{username}}", EscapeLdapFilterValue(user)); var allowedGroupDns = GetAllowedGroupDns(cfg); return await Task.Run(() => { try { using var searchConn = CreateConnection(cfg); searchConn.Bind(cfg.BindDN, cfg.BindPassword); var userDn = SearchUserDn(searchConn, cfg.SearchBase, filter); if (string.IsNullOrWhiteSpace(userDn)) { return new LdapVerifyResult(false, LdapVerifyFailReason.UserNotFound); } if (!IsMemberOfAllowedGroups(searchConn, userDn, cfg.SearchBase, allowedGroupDns)) { return new LdapVerifyResult(false, LdapVerifyFailReason.NotInAllowedGroup); } return new LdapVerifyResult(true, null); } catch { return new LdapVerifyResult(false, LdapVerifyFailReason.ConnectionError); } }); } }