diff --git a/DnsServerCore/Auth/AuthManager.cs b/DnsServerCore/Auth/AuthManager.cs index ae702313..58c97544 100644 --- a/DnsServerCore/Auth/AuthManager.cs +++ b/DnsServerCore/Auth/AuthManager.cs @@ -59,6 +59,20 @@ sealed class AuthManager : IDisposable bool _ssoAllowSignupOnlyForMappedUsers = true; IReadOnlyDictionary _ssoGroupMap; + bool _ldapEnabled; + string _ldapServer; + int _ldapPort = 389; + bool _ldapUseSsl; + bool _ldapIgnoreSslErrors; + string _ldapBindDn; + string _ldapBindPassword; + string _ldapSearchBase; + string _ldapUserFilter; + string _ldapGroupAttribute; + bool _ldapAllowSignup; + bool _ldapAllowSignupOnlyForMappedUsers = true; + IReadOnlyDictionary _ldapGroupMap; + readonly Lock _saveLock = new Lock(); bool _pendingSave; readonly Timer _saveTimer; @@ -249,6 +263,70 @@ private void LoadConfigFile() _ssoGroupMap = groupMap; } + string strLdapEnabled = Environment.GetEnvironmentVariable("DNS_SERVER_LDAP_ENABLED"); + if (!string.IsNullOrEmpty(strLdapEnabled)) + _ldapEnabled = bool.Parse(strLdapEnabled); + + string strLdapServer = Environment.GetEnvironmentVariable("DNS_SERVER_LDAP_SERVER"); + if (!string.IsNullOrEmpty(strLdapServer)) + _ldapServer = strLdapServer; + + string strLdapPort = Environment.GetEnvironmentVariable("DNS_SERVER_LDAP_PORT"); + if (!string.IsNullOrEmpty(strLdapPort)) + _ldapPort = int.Parse(strLdapPort); + + string strLdapUseSsl = Environment.GetEnvironmentVariable("DNS_SERVER_LDAP_USE_SSL"); + if (!string.IsNullOrEmpty(strLdapUseSsl)) + _ldapUseSsl = bool.Parse(strLdapUseSsl); + + string strLdapIgnoreSslErrors = Environment.GetEnvironmentVariable("DNS_SERVER_LDAP_IGNORE_SSL_ERRORS"); + if (!string.IsNullOrEmpty(strLdapIgnoreSslErrors)) + _ldapIgnoreSslErrors = bool.Parse(strLdapIgnoreSslErrors); + + string strLdapBindDn = Environment.GetEnvironmentVariable("DNS_SERVER_LDAP_BIND_DN"); + if (!string.IsNullOrEmpty(strLdapBindDn)) + _ldapBindDn = strLdapBindDn; + + string strLdapBindPassword = Environment.GetEnvironmentVariable("DNS_SERVER_LDAP_BIND_PASSWORD"); + if (!string.IsNullOrEmpty(strLdapBindPassword)) + _ldapBindPassword = strLdapBindPassword; + + string strLdapSearchBase = Environment.GetEnvironmentVariable("DNS_SERVER_LDAP_SEARCH_BASE"); + if (!string.IsNullOrEmpty(strLdapSearchBase)) + _ldapSearchBase = strLdapSearchBase; + + string strLdapUserFilter = Environment.GetEnvironmentVariable("DNS_SERVER_LDAP_USER_FILTER"); + if (!string.IsNullOrEmpty(strLdapUserFilter)) + _ldapUserFilter = strLdapUserFilter; + + string strLdapGroupAttribute = Environment.GetEnvironmentVariable("DNS_SERVER_LDAP_GROUP_ATTRIBUTE"); + if (!string.IsNullOrEmpty(strLdapGroupAttribute)) + _ldapGroupAttribute = strLdapGroupAttribute; + + string strLdapAllowSignup = Environment.GetEnvironmentVariable("DNS_SERVER_LDAP_ALLOW_SIGNUP"); + if (!string.IsNullOrEmpty(strLdapAllowSignup)) + _ldapAllowSignup = bool.Parse(strLdapAllowSignup); + + string strLdapAllowSignupOnlyForMappedUsers = Environment.GetEnvironmentVariable("DNS_SERVER_LDAP_ALLOW_SIGNUP_ONLY_FOR_MAPPED_USERS"); + if (!string.IsNullOrEmpty(strLdapAllowSignupOnlyForMappedUsers)) + _ldapAllowSignupOnlyForMappedUsers = bool.Parse(strLdapAllowSignupOnlyForMappedUsers); + + string strLdapGroupMap = Environment.GetEnvironmentVariable("DNS_SERVER_LDAP_GROUP_MAP"); + if (!string.IsNullOrEmpty(strLdapGroupMap)) + { + string[] entries = strLdapGroupMap.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + Dictionary groupMap = new Dictionary(entries.Length); + + foreach (string entry in entries) + { + string[] parts = entry.Split(':', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (parts.Length == 2) + groupMap.TryAdd(parts[0], parts[1]); + } + + _ldapGroupMap = groupMap; + } + SaveConfigFileInternal(); } catch (Exception ex) @@ -376,6 +454,7 @@ private void ReadConfigFrom(Stream s, bool isConfigTransfer, out bool restartWeb { case 1: case 2: + case 3: { int count = bR.ReadByte(); @@ -506,6 +585,55 @@ private void ReadConfigFrom(Stream s, bool isConfigTransfer, out bool restartWeb restartWebService = !ssoIsStillDisabled && restartWebService; } + if (version >= 3) + { + _ldapEnabled = bR.ReadBoolean(); + _ldapServer = s.ReadShortString(); + if (_ldapServer.Length == 0) + _ldapServer = null; + _ldapPort = bR.ReadInt32(); + _ldapUseSsl = bR.ReadBoolean(); + _ldapIgnoreSslErrors = bR.ReadBoolean(); + _ldapBindDn = s.ReadShortString(); + if (_ldapBindDn.Length == 0) + _ldapBindDn = null; + _ldapBindPassword = s.ReadShortString(); + if (_ldapBindPassword.Length == 0) + _ldapBindPassword = null; + _ldapSearchBase = s.ReadShortString(); + if (_ldapSearchBase.Length == 0) + _ldapSearchBase = null; + _ldapUserFilter = s.ReadShortString(); + if (_ldapUserFilter.Length == 0) + _ldapUserFilter = null; + _ldapGroupAttribute = s.ReadShortString(); + if (_ldapGroupAttribute.Length == 0) + _ldapGroupAttribute = null; + _ldapAllowSignup = bR.ReadBoolean(); + _ldapAllowSignupOnlyForMappedUsers = bR.ReadBoolean(); + + { + int count = bR.ReadByte(); + if (count > 0) + { + Dictionary ldapGroupMap = new Dictionary(count); + + for (int i = 0; i < count; i++) + { + string key = s.ReadShortString(); + string value = s.ReadShortString(); + ldapGroupMap.TryAdd(key, value); + } + + _ldapGroupMap = ldapGroupMap; + } + else + { + _ldapGroupMap = null; + } + } + } + break; default: @@ -576,7 +704,7 @@ private void WriteConfigTo(Stream s) BinaryWriter bW = new BinaryWriter(s); bW.Write(Encoding.ASCII.GetBytes("AS")); //format - bW.Write((byte)2); //version + bW.Write((byte)3); //version bW.Write(Convert.ToByte(_groups.Count)); @@ -647,6 +775,35 @@ private void WriteConfigTo(Stream s) s.WriteShortString(entry.Value); } } + + // LDAP config (version 3+) + bW.Write(_ldapEnabled); + s.WriteShortString(_ldapServer ?? ""); + bW.Write(_ldapPort); + bW.Write(_ldapUseSsl); + bW.Write(_ldapIgnoreSslErrors); + s.WriteShortString(_ldapBindDn ?? ""); + s.WriteShortString(_ldapBindPassword ?? ""); + s.WriteShortString(_ldapSearchBase ?? ""); + s.WriteShortString(_ldapUserFilter ?? ""); + s.WriteShortString(_ldapGroupAttribute ?? ""); + bW.Write(_ldapAllowSignup); + bW.Write(_ldapAllowSignupOnlyForMappedUsers); + + if ((_ldapGroupMap is null) || (_ldapGroupMap.Count == 0)) + { + bW.Write((byte)0); + } + else + { + bW.Write(Convert.ToByte(_ldapGroupMap.Count)); + + foreach (KeyValuePair entry in _ldapGroupMap) + { + s.WriteShortString(entry.Key); + s.WriteShortString(entry.Value); + } + } } #endregion @@ -735,7 +892,137 @@ private async Task AuthenticateUserAsync(string username, string password, User user = GetUser(username); - if ((user is null) || user.IsSsoUser || !user.PasswordHash.Equals(user.GetPasswordHashFor(password), StringComparison.Ordinal)) + if (user is not null && user.IsSsoUser) + { + // OIDC SSO users cannot authenticate via password + MarkFailedLoginAttempt(network); + + if (HasLoginAttemptExceedLimit(network, MAX_LOGIN_ATTEMPTS)) + BlockNetwork(network, BLOCK_NETWORK_INTERVAL); + + await Task.Delay(1000); + + throw new DnsWebServiceException("Invalid username or password for user: " + username); + } + + if (user is not null && user.IsLdapUser) + { + // Existing LDAP user — validate credentials against LDAP directory + if (!_ldapEnabled || string.IsNullOrEmpty(_ldapServer)) + { + await Task.Delay(1000); + throw new DnsWebServiceException("LDAP authentication is not configured."); + } + + LdapAuthProvider ldapProvider = new LdapAuthProvider(_ldapServer, _ldapPort, _ldapUseSsl, _ldapIgnoreSslErrors, _ldapBindDn, _ldapBindPassword, _ldapSearchBase, _ldapUserFilter, _ldapGroupAttribute); + LdapAuthResult ldapResult = await ldapProvider.AuthenticateAsync(username, password); + + if (!ldapResult.Success) + { + MarkFailedLoginAttempt(network); + + if (HasLoginAttemptExceedLimit(network, MAX_LOGIN_ATTEMPTS)) + BlockNetwork(network, BLOCK_NETWORK_INTERVAL); + + await Task.Delay(1000); + + throw new DnsWebServiceException("Invalid username or password for user: " + username); + } + + ResetFailedLoginAttempts(network); + + if (user.Disabled) + throw new DnsWebServiceException("User account is disabled. Please contact your administrator."); + + // Sync display name and group memberships from LDAP + user.DisplayName = ldapResult.DisplayName; + + if (_ldapGroupMap is not null) + { + Dictionary mappedGroups = new Dictionary(); + Group everyoneGroup = GetGroup(Group.EVERYONE); + if (everyoneGroup is not null) + mappedGroups[everyoneGroup.Name.ToLowerInvariant()] = everyoneGroup; + + foreach (string remoteGroup in ldapResult.Groups) + { + if (_ldapGroupMap.TryGetValue(remoteGroup, out string localGroupName)) + { + Group localGroup = GetGroup(localGroupName); + if (localGroup is not null) + mappedGroups[localGroup.Name.ToLowerInvariant()] = localGroup; + } + } + + user.SyncGroups(mappedGroups); + } + + return user; + } + + if (user is null && _ldapEnabled && !string.IsNullOrEmpty(_ldapServer)) + { + // No local account found — try LDAP authentication and auto-provision if allowed + LdapAuthProvider ldapProvider = new LdapAuthProvider(_ldapServer, _ldapPort, _ldapUseSsl, _ldapIgnoreSslErrors, _ldapBindDn, _ldapBindPassword, _ldapSearchBase, _ldapUserFilter, _ldapGroupAttribute); + LdapAuthResult ldapResult = await ldapProvider.AuthenticateAsync(username, password); + + if (ldapResult.Success) + { + if (!_ldapAllowSignup) + { + _log.Write(new System.Net.IPEndPoint(remoteAddress, 0), "LDAP authentication succeeded for '" + username + "' but new user sign up is disabled."); + await Task.Delay(1000); + throw new DnsWebServiceException("LDAP authentication succeeded but new user sign up is disabled. Please contact your administrator."); + } + + if (_ldapAllowSignupOnlyForMappedUsers && _ldapGroupMap is not null) + { + bool hasMappedGroup = false; + + foreach (string remoteGroup in ldapResult.Groups) + { + if (_ldapGroupMap.ContainsKey(remoteGroup)) + { + hasMappedGroup = true; + break; + } + } + + if (!hasMappedGroup) + { + _log.Write(new System.Net.IPEndPoint(remoteAddress, 0), "LDAP authentication succeeded for '" + username + "' but new user sign up is restricted to mapped groups only."); + await Task.Delay(1000); + throw new DnsWebServiceException("LDAP authentication succeeded but new user sign up is restricted only to members of mapped groups. Please contact your administrator."); + } + } + + // Provision new LDAP user + string localUsername = username.ToLowerInvariant(); + user = CreateLdapUser(ldapResult.DisplayName, localUsername, ldapResult.LdapIdentifier); + + if (_ldapGroupMap is not null) + { + foreach (string remoteGroup in ldapResult.Groups) + { + if (_ldapGroupMap.TryGetValue(remoteGroup, out string localGroupName)) + { + Group localGroup = GetGroup(localGroupName); + if (localGroup is not null) + user.AddToGroup(localGroup); + } + } + } + + _log.Write(new System.Net.IPEndPoint(remoteAddress, 0), "LDAP user account was created successfully with username: " + user.Username); + SaveConfigFile(); + + ResetFailedLoginAttempts(network); + return user; + } + } + + // Local password authentication (or final failure) + if ((user is null) || !user.PasswordHash.Equals(user.GetPasswordHashFor(password), StringComparison.Ordinal)) { if ((username != "admin") || (password != "admin")) { @@ -865,6 +1152,41 @@ public User GetSsoUser(string ssoIdentifier) return null; } + public User GetLdapUser(string ldapIdentifier) + { + foreach (KeyValuePair user in _users) + { + if (ldapIdentifier.Equals(user.Value.LdapIdentifier, StringComparison.OrdinalIgnoreCase) && user.Value.IsLdapUser) + return user.Value; + } + + return null; + } + + public User CreateLdapUser(string displayName, string username, string ldapIdentifier) + { + if (_users.Count >= byte.MaxValue) + throw new DnsWebServiceException("Cannot create more than 255 users."); + + username = username.ToLowerInvariant(); + + User user = User.CreateLdapUser(displayName, username, ldapIdentifier); + + if (_users.TryAdd(username, user)) + { + if (_users.Count > byte.MaxValue) + { + _users.TryRemove(username, out _); //undo + throw new DnsWebServiceException("Cannot create more than 255 users."); + } + + user.AddToGroup(GetGroup(Group.EVERYONE)); + return user; + } + + throw new DnsWebServiceException("User already exists: " + username); + } + public User CreateUser(string displayName, string username, string password, int iterations = User.DEFAULT_ITERATIONS) { if (_users.Count >= byte.MaxValue) @@ -1372,6 +1694,133 @@ public IReadOnlyDictionary SsoGroupMap public bool SsoManagedGroups { get { return _ssoGroupMap is not null; } } + public bool LdapEnabled + { + get { return _ldapEnabled; } + set { _ldapEnabled = value; } + } + + public string LdapServer + { + get { return _ldapServer; } + set + { + if (value is not null && value.Length == 0) + value = null; + _ldapServer = value; + } + } + + public int LdapPort + { + get { return _ldapPort; } + set + { + if ((value < 1) || (value > 65535)) + throw new ArgumentOutOfRangeException(nameof(LdapPort), "LDAP port must be between 1 and 65535."); + _ldapPort = value; + } + } + + public bool LdapUseSsl + { + get { return _ldapUseSsl; } + set { _ldapUseSsl = value; } + } + + public bool LdapIgnoreSslErrors + { + get { return _ldapIgnoreSslErrors; } + set { _ldapIgnoreSslErrors = value; } + } + + public string LdapBindDn + { + get { return _ldapBindDn; } + set + { + if (value is not null && value.Length == 0) + value = null; + _ldapBindDn = value; + } + } + + public string LdapBindPassword + { + get { return _ldapBindPassword; } + set + { + if (value is not null && value.Length == 0) + value = null; + _ldapBindPassword = value; + } + } + + public string LdapSearchBase + { + get { return _ldapSearchBase; } + set + { + if (value is not null && value.Length == 0) + value = null; + _ldapSearchBase = value; + } + } + + public string LdapUserFilter + { + get { return _ldapUserFilter; } + set + { + if (value is not null && value.Length == 0) + value = null; + _ldapUserFilter = value; + } + } + + public string LdapGroupAttribute + { + get { return _ldapGroupAttribute; } + set + { + if (value is not null && value.Length == 0) + value = null; + _ldapGroupAttribute = value; + } + } + + public bool LdapAllowSignup + { + get { return _ldapAllowSignup; } + set { _ldapAllowSignup = value; } + } + + public bool LdapAllowSignupOnlyForMappedUsers + { + get { return _ldapAllowSignupOnlyForMappedUsers; } + set { _ldapAllowSignupOnlyForMappedUsers = value; } + } + + public IReadOnlyDictionary LdapGroupMap + { + get { return _ldapGroupMap; } + set + { + if (value is not null) + { + if (value.Count == 0) + value = null; + else if (value.Count > 255) + throw new ArgumentException("The LDAP Group Map cannot have more than 255 entries.", nameof(LdapGroupMap)); + } + + _ldapGroupMap = value; + } + } + + public bool LdapManagedGroups + { get { return _ldapGroupMap is not null; } } + #endregion } } diff --git a/DnsServerCore/Auth/LdapAuthProvider.cs b/DnsServerCore/Auth/LdapAuthProvider.cs new file mode 100644 index 00000000..fdeda0fe --- /dev/null +++ b/DnsServerCore/Auth/LdapAuthProvider.cs @@ -0,0 +1,251 @@ +/* +Technitium DNS Server +Copyright (C) 2026 Shreyas Zare (shreyas@technitium.com) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +using Novell.Directory.Ldap; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace DnsServerCore.Auth +{ + sealed class LdapAuthResult + { + public bool Success { get; init; } + public string LdapIdentifier { get; init; } + public string DisplayName { get; init; } + public IReadOnlyList Groups { get; init; } + public string ErrorMessage { get; init; } + + public static LdapAuthResult Failed(string message) => + new LdapAuthResult { Success = false, ErrorMessage = message }; + } + + sealed class LdapAuthProvider + { + #region variables + + readonly string _server; + readonly int _port; + readonly bool _useSsl; + readonly bool _ignoreSslErrors; + readonly string _bindDn; + readonly string _bindPassword; + readonly string _searchBase; + readonly string _userFilter; + readonly string _groupAttribute; + + #endregion + + #region constructor + + public LdapAuthProvider(string server, int port, bool useSsl, bool ignoreSslErrors, string bindDn, string bindPassword, string searchBase, string userFilter, string groupAttribute) + { + _server = server; + _port = port; + _useSsl = useSsl; + _ignoreSslErrors = ignoreSslErrors; + _bindDn = bindDn; + _bindPassword = bindPassword; + _searchBase = searchBase; + _userFilter = string.IsNullOrWhiteSpace(userFilter) ? "(sAMAccountName={0})" : userFilter; + _groupAttribute = string.IsNullOrWhiteSpace(groupAttribute) ? "memberOf" : groupAttribute; + } + + #endregion + + #region private + + private LdapConnection CreateConnection() + { + var options = new LdapConnectionOptions(); + + if (_ignoreSslErrors) + options = options.ConfigureRemoteCertificateValidationCallback((sender, cert, chain, errors) => true); + + // Port 636 = LDAPS (SSL-wrapped from the start); all other ports use StartTLS + bool useLdaps = _useSsl && _port == 636; + if (useLdaps) + options = options.UseSsl(); + + var conn = new LdapConnection(options); + conn.Connect(_server, _port); + + if (_useSsl && !useLdaps) + conn.StartTls(); + + return conn; + } + + private static string LdapFilterEscape(string value) + { + // RFC 4515 escape special filter characters + return new StringBuilder(value) + .Replace("\\", "\\5c") + .Replace("*", "\\2a") + .Replace("(", "\\28") + .Replace(")", "\\29") + .Replace("\0", "\\00") + .ToString(); + } + + private static string GetCnFromDn(string dn) + { + if (string.IsNullOrEmpty(dn)) + return dn; + + int eq = dn.IndexOf('='); + int comma = dn.IndexOf(','); + + if (eq < 0) + return dn; + + int end = comma > eq ? comma : dn.Length; + return dn.Substring(eq + 1, end - eq - 1).Trim(); + } + + #endregion + + #region public + + public Task AuthenticateAsync(string username, string password) + { + return Task.Run(() => + { + // Step 1: bind service account and search for the user + string userDn; + string displayName; + string userPrincipalName; + List groups; + + try + { + using LdapConnection searchConn = CreateConnection(); + searchConn.Bind(LdapConnection.LdapV3, _bindDn, _bindPassword); + + string filter = string.Format(_userFilter, LdapFilterEscape(username)); + string[] attrs = new[] { "distinguishedName", "cn", "displayName", "userPrincipalName", _groupAttribute }; + + var searchConstraints = new LdapSearchConstraints { ReferralFollowing = false, TimeLimit = 15000, ServerTimeLimit = 15 }; + ILdapSearchResults results = searchConn.Search( + _searchBase, + LdapConnection.ScopeSub, + filter, + attrs, + false, + searchConstraints); + + LdapEntry entry = null; + while (results.HasMore()) + { + LdapEntry candidate; + try { candidate = results.Next(); } + catch (LdapReferralException) { continue; } + entry = candidate; + break; + } + + if (entry is null) + return LdapAuthResult.Failed("User not found in directory."); + LdapAttributeSet attrSet = entry.GetAttributeSet(); + + userDn = entry.Dn; + + displayName = null; + if (attrSet.ContainsKey("displayName")) + displayName = attrSet["displayName"].StringValue; + if (string.IsNullOrEmpty(displayName) && attrSet.ContainsKey("cn")) + displayName = attrSet["cn"].StringValue; + if (string.IsNullOrEmpty(displayName)) + displayName = username; + + userPrincipalName = null; + if (attrSet.ContainsKey("userPrincipalName")) + userPrincipalName = attrSet["userPrincipalName"].StringValue; + + groups = new List(); + if (attrSet.ContainsKey(_groupAttribute)) + { + foreach (string groupDn in attrSet[_groupAttribute].StringValueArray) + { + string cn = GetCnFromDn(groupDn); + if (!string.IsNullOrEmpty(cn)) + groups.Add(cn); + } + } + } + catch (LdapException ex) when (ex.ResultCode == LdapException.InvalidCredentials) + { + return LdapAuthResult.Failed("Service account credentials are invalid."); + } + catch (Exception ex) + { + return LdapAuthResult.Failed($"Service account bind/search failed: {ex.Message}"); + } + + // Step 2: re-bind as the user to validate their password + // Prefer UPN (user@domain) over full DN — more reliable with AD + string bindUsername = !string.IsNullOrEmpty(userPrincipalName) ? userPrincipalName : userDn; + + try + { + using LdapConnection userConn = CreateConnection(); + userConn.Bind(LdapConnection.LdapV3, bindUsername, password); + } + catch (LdapException ex) when (ex.ResultCode == LdapException.InvalidCredentials) + { + return LdapAuthResult.Failed("Invalid credentials."); + } + catch (Exception ex) + { + return LdapAuthResult.Failed($"User bind failed: {ex.Message}"); + } + + return new LdapAuthResult + { + Success = true, + LdapIdentifier = userDn, + DisplayName = displayName, + Groups = groups + }; + }); + } + + public Task TestConnectionAsync() + { + return Task.Run(() => + { + try + { + using LdapConnection conn = CreateConnection(); + conn.Bind(LdapConnection.LdapV3, _bindDn, _bindPassword); + return (string)null; // null = success + } + catch (Exception ex) + { + Exception inner = ex; + while (inner.InnerException != null) inner = inner.InnerException; + return inner == ex ? ex.Message : $"{ex.Message} → {inner.GetType().Name}: {inner.Message}"; + } + }); + } + + #endregion + } +} diff --git a/DnsServerCore/Auth/User.cs b/DnsServerCore/Auth/User.cs index 4fa4731b..6e940c87 100644 --- a/DnsServerCore/Auth/User.cs +++ b/DnsServerCore/Auth/User.cs @@ -47,6 +47,8 @@ class User : IComparable string _username; bool _isSsoUser; string _ssoIdentifier; + bool _isLdapUser; + string _ldapIdentifier; UserPasswordHashType _passwordHashType; int _iterations; byte[] _salt; @@ -83,6 +85,7 @@ public User(BinaryReader bR, IReadOnlyDictionary groups) case 1: case 2: case 3: + case 4: _displayName = bR.BaseStream.ReadShortString(); _username = bR.BaseStream.ReadShortString(); @@ -95,18 +98,28 @@ public User(BinaryReader bR, IReadOnlyDictionary groups) } else { - _passwordHashType = (UserPasswordHashType)bR.ReadByte(); - _iterations = bR.ReadInt32(); - _salt = bR.ReadBuffer(); - _passwordHash = bR.BaseStream.ReadShortString(); + if (version >= 4) + _isLdapUser = bR.ReadBoolean(); - if (version >= 2) + if (_isLdapUser) { - string otpKeyUri = bR.ReadString(); - if (!string.IsNullOrEmpty(otpKeyUri)) - _totpKeyUri = AuthenticatorKeyUri.Parse(otpKeyUri); - - _totpEnabled = bR.ReadBoolean(); + _ldapIdentifier = bR.BaseStream.ReadShortString(); + } + else + { + _passwordHashType = (UserPasswordHashType)bR.ReadByte(); + _iterations = bR.ReadInt32(); + _salt = bR.ReadBuffer(); + _passwordHash = bR.BaseStream.ReadShortString(); + + if (version >= 2) + { + string otpKeyUri = bR.ReadString(); + if (!string.IsNullOrEmpty(otpKeyUri)) + _totpKeyUri = AuthenticatorKeyUri.Parse(otpKeyUri); + + _totpEnabled = bR.ReadBoolean(); + } } } @@ -163,6 +176,18 @@ public static User CreateSsoUser(string displayName, string username, string sso return user; } + public static User CreateLdapUser(string displayName, string username, string ldapIdentifier) + { + User user = new User(); + + user.SetUsername(username); + user.DisplayName = displayName; + user._isLdapUser = true; + user._ldapIdentifier = ldapIdentifier; + + return user; + } + public static bool IsUsernameValid(string username, bool throwException = false) { if (string.IsNullOrWhiteSpace(username)) @@ -260,6 +285,9 @@ public void ChangePassword(string newPassword, int iterations = DEFAULT_ITERATIO if (_isSsoUser) throw new InvalidOperationException("Cannot change password for SSO users."); + if (_isLdapUser) + throw new InvalidOperationException("Cannot change password for LDAP users."); + _passwordHashType = UserPasswordHashType.PBKDF2_SHA256; _iterations = iterations; @@ -274,6 +302,9 @@ public void LoadOldSchemeCredentials(string passwordHash) if (_isSsoUser) throw new InvalidOperationException(); + if (_isLdapUser) + throw new InvalidOperationException(); + _passwordHashType = UserPasswordHashType.OldScheme; _passwordHash = passwordHash; } @@ -283,6 +314,9 @@ public AuthenticatorKeyUri InitializedTOTP(string issuer) if (_isSsoUser) throw new InvalidOperationException("Time-based one-time password (TOTP) feature is not available for SSO users."); + if (_isLdapUser) + throw new InvalidOperationException("Time-based one-time password (TOTP) feature is not available for LDAP users."); + if (_totpEnabled) throw new InvalidOperationException("Time-based one-time password (TOTP) is already enabled for user: " + _username); @@ -296,6 +330,9 @@ public void EnableTOTP(string totp) if (_isSsoUser) throw new InvalidOperationException("Time-based one-time password (TOTP) feature is not available for SSO users."); + if (_isLdapUser) + throw new InvalidOperationException("Time-based one-time password (TOTP) feature is not available for LDAP users."); + if (_totpKeyUri is null) throw new InvalidOperationException("Time-based one-time password (TOTP) was not initialized for user: " + _username); @@ -315,6 +352,9 @@ public void DisableTOTP() if (_isSsoUser) throw new InvalidOperationException("Time-based one-time password (TOTP) feature is not available for SSO users."); + if (_isLdapUser) + throw new InvalidOperationException("Time-based one-time password (TOTP) feature is not available for LDAP users."); + if (!_totpEnabled) throw new InvalidOperationException("Time-based one-time password (TOTP) is already disabled for user: " + _username); @@ -371,7 +411,7 @@ public bool IsMemberOfGroup(Group group) public void WriteTo(BinaryWriter bW) { - bW.Write((byte)3); + bW.Write((byte)4); bW.BaseStream.WriteShortString(_displayName); bW.BaseStream.WriteShortString(_username); @@ -383,17 +423,26 @@ public void WriteTo(BinaryWriter bW) } else { - bW.Write((byte)_passwordHashType); - bW.Write(_iterations); - bW.WriteBuffer(_salt); - bW.BaseStream.WriteShortString(_passwordHash); + bW.Write(_isLdapUser); - if (_totpKeyUri is null) - bW.Write(""); + if (_isLdapUser) + { + bW.BaseStream.WriteShortString(_ldapIdentifier); + } else - bW.Write(_totpKeyUri.ToString()); + { + bW.Write((byte)_passwordHashType); + bW.Write(_iterations); + bW.WriteBuffer(_salt); + bW.BaseStream.WriteShortString(_passwordHash); + + if (_totpKeyUri is null) + bW.Write(""); + else + bW.Write(_totpKeyUri.ToString()); - bW.Write(_totpEnabled); + bW.Write(_totpEnabled); + } } bW.Write(_disabled); @@ -460,6 +509,12 @@ public bool IsSsoUser public string SsoIdentifier { get { return _ssoIdentifier; } } + public bool IsLdapUser + { get { return _isLdapUser; } } + + public string LdapIdentifier + { get { return _ldapIdentifier; } } + public UserPasswordHashType PasswordHashType { get { return _passwordHashType; } } diff --git a/DnsServerCore/DnsServerCore.csproj b/DnsServerCore/DnsServerCore.csproj index 6d710843..f0a6e2e2 100644 --- a/DnsServerCore/DnsServerCore.csproj +++ b/DnsServerCore/DnsServerCore.csproj @@ -47,6 +47,7 @@ + diff --git a/DnsServerCore/DnsWebService.cs b/DnsServerCore/DnsWebService.cs index df5ad6ca..623015a3 100644 --- a/DnsServerCore/DnsWebService.cs +++ b/DnsServerCore/DnsWebService.cs @@ -1993,6 +1993,9 @@ private void ConfigureWebServiceRoutes() _webService.MapGetAndPost("/api/admin/sso/set", _authApi.SetSsoConfig); _webService.MapGetAndPost("/api/admin/sso/users/create", _authApi.CreateSsoUser); _webService.MapGetAndPost("/api/admin/sso/users/set", _authApi.SetSsoUser); + _webService.MapGetAndPost("/api/admin/ldap/get", _authApi.GetLdapConfig); + _webService.MapGetAndPost("/api/admin/ldap/set", _authApi.SetLdapConfig); + _webService.MapGetAndPost("/api/admin/ldap/test", _authApi.TestLdapConnectionAsync); _webService.MapGetAndPost("/api/admin/cluster/state", _clusterApi.GetClusterState); _webService.MapGetAndPost("/api/admin/cluster/init", _clusterApi.InitializeCluster); _webService.MapGetAndPost("/api/admin/cluster/primary/delete", _clusterApi.DeleteCluster); @@ -2064,6 +2067,7 @@ private static ClusterNodeType GetClusterNodeTypeForPath(string path) case "/api/admin/sso/set": case "/api/admin/sso/users/create": case "/api/admin/sso/users/set": + case "/api/admin/ldap/set": return ClusterNodeType.Primary; //this api can be called only on primary node case "/sso/login": diff --git a/DnsServerCore/WebServiceAuthApi.cs b/DnsServerCore/WebServiceAuthApi.cs index b329274f..2e300289 100644 --- a/DnsServerCore/WebServiceAuthApi.cs +++ b/DnsServerCore/WebServiceAuthApi.cs @@ -72,8 +72,9 @@ private void WriteCurrentSessionDetails(Utf8JsonWriter jsonWriter, UserSession c jsonWriter.WriteString("displayName", currentSession.User.DisplayName); jsonWriter.WriteString("username", currentSession.User.Username); jsonWriter.WriteBoolean("isSsoUser", currentSession.User.IsSsoUser); + jsonWriter.WriteBoolean("isLdapUser", currentSession.User.IsLdapUser); - if (!currentSession.User.IsSsoUser) + if (!currentSession.User.IsSsoUser && !currentSession.User.IsLdapUser) jsonWriter.WriteBoolean("totpEnabled", currentSession.User.TOTPEnabled); jsonWriter.WriteString("token", currentSession.Token); @@ -129,8 +130,9 @@ private void WriteUserDetails(Utf8JsonWriter jsonWriter, User user, UserSession jsonWriter.WriteString("displayName", user.DisplayName); jsonWriter.WriteString("username", user.Username); jsonWriter.WriteBoolean("isSsoUser", user.IsSsoUser); + jsonWriter.WriteBoolean("isLdapUser", user.IsLdapUser); - if (!user.IsSsoUser) + if (!user.IsSsoUser && !user.IsLdapUser) jsonWriter.WriteBoolean("totpEnabled", user.TOTPEnabled); jsonWriter.WriteBoolean("disabled", user.Disabled); @@ -143,6 +145,7 @@ private void WriteUserDetails(Utf8JsonWriter jsonWriter, User user, UserSession { jsonWriter.WriteNumber("sessionTimeoutSeconds", user.SessionTimeoutSeconds); jsonWriter.WriteBoolean("ssoManagedGroups", _dnsWebService._authManager.SsoManagedGroups); + jsonWriter.WriteBoolean("ldapManagedGroups", _dnsWebService._authManager.LdapManagedGroups); jsonWriter.WritePropertyName("memberOfGroups"); jsonWriter.WriteStartArray(); @@ -1392,6 +1395,9 @@ public void SetGroupDetails(HttpContext context) if (ssoManagedGroups && user.IsSsoUser && !user.IsMemberOfGroup(group)) throw new DnsWebServiceException("Cannot add user '" + user.Username + "' since group memberships for SSO users are managed by the SSO provider."); + if (_dnsWebService._authManager.LdapManagedGroups && user.IsLdapUser && !user.IsMemberOfGroup(group)) + throw new DnsWebServiceException("Cannot add user '" + user.Username + "' since group memberships for LDAP users are managed by the LDAP directory."); + users.Add(user.Username, user); } @@ -1903,6 +1909,172 @@ public void SetSsoUser(HttpContext context) WriteUserDetails(jsonWriter, user, null, false, false); } + private void WriteLdapConfig(Utf8JsonWriter jsonWriter, bool includeGroups) + { + jsonWriter.WriteBoolean("ldapEnabled", _dnsWebService._authManager.LdapEnabled); + jsonWriter.WriteString("ldapServer", _dnsWebService._authManager.LdapServer); + jsonWriter.WriteNumber("ldapPort", _dnsWebService._authManager.LdapPort); + jsonWriter.WriteBoolean("ldapUseSsl", _dnsWebService._authManager.LdapUseSsl); + jsonWriter.WriteBoolean("ldapIgnoreSslErrors", _dnsWebService._authManager.LdapIgnoreSslErrors); + jsonWriter.WriteString("ldapBindDn", _dnsWebService._authManager.LdapBindDn); + + if (string.IsNullOrEmpty(_dnsWebService._authManager.LdapBindPassword)) + jsonWriter.WriteString("ldapBindPassword", null as string); + else + jsonWriter.WriteString("ldapBindPassword", "************"); + + jsonWriter.WriteString("ldapSearchBase", _dnsWebService._authManager.LdapSearchBase); + jsonWriter.WriteString("ldapUserFilter", _dnsWebService._authManager.LdapUserFilter); + jsonWriter.WriteString("ldapGroupAttribute", _dnsWebService._authManager.LdapGroupAttribute); + jsonWriter.WriteBoolean("ldapAllowSignup", _dnsWebService._authManager.LdapAllowSignup); + jsonWriter.WriteBoolean("ldapAllowSignupOnlyForMappedUsers", _dnsWebService._authManager.LdapAllowSignupOnlyForMappedUsers); + + jsonWriter.WriteStartArray("ldapGroupMap"); + + IReadOnlyDictionary ldapGroupMap = _dnsWebService._authManager.LdapGroupMap; + if (ldapGroupMap is not null) + { + foreach (KeyValuePair entry in ldapGroupMap) + { + jsonWriter.WriteStartObject(); + jsonWriter.WriteString("remoteGroup", entry.Key); + jsonWriter.WriteString("localGroup", entry.Value); + jsonWriter.WriteEndObject(); + } + } + + jsonWriter.WriteEndArray(); + + if (includeGroups) + { + List groups = new List(_dnsWebService._authManager.Groups); + groups.Sort(); + + jsonWriter.WriteStartArray("localGroups"); + + foreach (Group group in groups) + { + if (group.Name.Equals("Everyone", StringComparison.OrdinalIgnoreCase)) + continue; + + jsonWriter.WriteStringValue(group.Name); + } + + jsonWriter.WriteEndArray(); + } + } + + public void GetLdapConfig(HttpContext context) + { + User sessionUser = _dnsWebService.GetSessionUser(context); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.View)) + throw new DnsWebServiceException("Access was denied."); + + bool includeGroups = context.Request.GetQueryOrForm("includeGroups", bool.Parse, false); + + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); + WriteLdapConfig(jsonWriter, includeGroups); + } + + public void SetLdapConfig(HttpContext context) + { + User sessionUser = _dnsWebService.GetSessionUser(context); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Delete)) + throw new DnsWebServiceException("Access was denied."); + + HttpRequest request = context.Request; + + if (request.TryGetQueryOrForm("ldapEnabled", bool.Parse, out bool ldapEnabled)) + _dnsWebService._authManager.LdapEnabled = ldapEnabled; + + if (request.TryQueryOrForm("ldapServer", out string ldapServer)) + _dnsWebService._authManager.LdapServer = ldapServer; + + if (request.TryGetQueryOrForm("ldapPort", int.Parse, out int ldapPort)) + _dnsWebService._authManager.LdapPort = ldapPort; + + if (request.TryGetQueryOrForm("ldapUseSsl", bool.Parse, out bool ldapUseSsl)) + _dnsWebService._authManager.LdapUseSsl = ldapUseSsl; + + if (request.TryGetQueryOrForm("ldapIgnoreSslErrors", bool.Parse, out bool ldapIgnoreSslErrors)) + _dnsWebService._authManager.LdapIgnoreSslErrors = ldapIgnoreSslErrors; + + if (request.TryQueryOrForm("ldapBindDn", out string ldapBindDn)) + _dnsWebService._authManager.LdapBindDn = ldapBindDn; + + if (request.TryQueryOrForm("ldapBindPassword", out string ldapBindPassword)) + { + if (ldapBindPassword != "************") + _dnsWebService._authManager.LdapBindPassword = ldapBindPassword; + } + + if (request.TryQueryOrForm("ldapSearchBase", out string ldapSearchBase)) + _dnsWebService._authManager.LdapSearchBase = ldapSearchBase; + + if (request.TryQueryOrForm("ldapUserFilter", out string ldapUserFilter)) + _dnsWebService._authManager.LdapUserFilter = ldapUserFilter; + + if (request.TryQueryOrForm("ldapGroupAttribute", out string ldapGroupAttribute)) + _dnsWebService._authManager.LdapGroupAttribute = ldapGroupAttribute; + + if (request.TryGetQueryOrForm("ldapAllowSignup", bool.Parse, out bool ldapAllowSignup)) + _dnsWebService._authManager.LdapAllowSignup = ldapAllowSignup; + + if (request.TryGetQueryOrForm("ldapAllowSignupOnlyForMappedUsers", bool.Parse, out bool ldapAllowSignupOnlyForMappedUsers)) + _dnsWebService._authManager.LdapAllowSignupOnlyForMappedUsers = ldapAllowSignupOnlyForMappedUsers; + + if (request.TryQueryOrFormArray("ldapGroupMap", delegate (ArraySegment tableRow) + { + return new KeyValuePair(tableRow[0], tableRow[1]); + }, 2, out KeyValuePair[] ldapGroupMapEntries, '|')) + { + _dnsWebService._authManager.LdapGroupMap = new Dictionary(ldapGroupMapEntries); + } + + _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] LDAP config was updated successfully."); + + _dnsWebService._authManager.SaveConfigFile(); + + if (_dnsWebService._clusterManager.ClusterInitialized) + _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode(); + + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); + WriteLdapConfig(jsonWriter, false); + } + + public async Task TestLdapConnectionAsync(HttpContext context) + { + User sessionUser = _dnsWebService.GetSessionUser(context); + + if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.View)) + throw new DnsWebServiceException("Access was denied."); + + string server = context.Request.GetQueryOrForm("ldapServer", _dnsWebService._authManager.LdapServer); + int port = context.Request.GetQueryOrForm("ldapPort", int.Parse, _dnsWebService._authManager.LdapPort); + bool useSsl = context.Request.GetQueryOrForm("ldapUseSsl", bool.Parse, _dnsWebService._authManager.LdapUseSsl); + bool ignoreSslErrors = context.Request.GetQueryOrForm("ldapIgnoreSslErrors", bool.Parse, _dnsWebService._authManager.LdapIgnoreSslErrors); + string bindDn = context.Request.GetQueryOrForm("ldapBindDn", _dnsWebService._authManager.LdapBindDn); + string bindPassword = context.Request.GetQueryOrForm("ldapBindPassword", _dnsWebService._authManager.LdapBindPassword); + string searchBase = context.Request.GetQueryOrForm("ldapSearchBase", _dnsWebService._authManager.LdapSearchBase); + string userFilter = context.Request.GetQueryOrForm("ldapUserFilter", _dnsWebService._authManager.LdapUserFilter); + string groupAttribute = context.Request.GetQueryOrForm("ldapGroupAttribute", _dnsWebService._authManager.LdapGroupAttribute); + + if (string.IsNullOrEmpty(server)) + throw new DnsWebServiceException("LDAP Server is required for connection test."); + + if (bindPassword == "************") + bindPassword = _dnsWebService._authManager.LdapBindPassword; + + LdapAuthProvider provider = new LdapAuthProvider(server, port, useSsl, ignoreSslErrors, bindDn, bindPassword, searchBase, userFilter, groupAttribute); + string error = await provider.TestConnectionAsync(); + + Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); + jsonWriter.WriteBoolean("success", error is null); + jsonWriter.WriteString("message", error is null ? "Connection successful." : error); + } + #endregion } } diff --git a/DnsServerCore/www/index.html b/DnsServerCore/www/index.html index 9a76064a..ea458be2 100644 --- a/DnsServerCore/www/index.html +++ b/DnsServerCore/www/index.html @@ -2807,6 +2807,7 @@

Edit Scope

+ @@ -3033,6 +3034,149 @@

Edit Scope

+
+
+ +
+
+
+ +
+
+ +
+
Enable to allow users from an LDAP directory (Active Directory, OpenLDAP, etc.) to log in using their directory credentials.
+
+
+ +
+ +
+ +
Hostname or IP address of the LDAP server.
+
+
+ +
+ +
+ +
Use 389 for plain LDAP or StartTLS, 636 for LDAPS.
+
+
+ +
+ +
+
+ +
+
+ +
+
Warning! Ignoring SSL errors is insecure and should only be used for testing.
+
+
+ +
+ +
+ +
Distinguished Name of a service account used to search the directory. Leave empty for anonymous bind.
+
+
+ +
+ +
+ +
+
+ +
+ +
+ +
The base Distinguished Name used to search for user entries.
+
+
+ +
+ +
+ +
LDAP search filter used to find the user. Use {0} as a placeholder for the username. Default: (sAMAccountName={0}) for Active Directory, (uid={0}) for OpenLDAP.
+
+
+ +
+ +
+ +
The user attribute listing group memberships. Default: memberOf for Active Directory.
+
+
+ +
+ +
+
+ +
+
Enable to automatically create a local account for LDAP users on first login.
+ +
+ +
+
Restrict auto-provisioning to users who are members of at least one mapped group.
+
+
+ +
+ +
+ + + + + + + + + +
Remote Group (LDAP CN)Local Group + +
+
Map LDAP groups (by CN name) to local Technitium groups. Group memberships are synced on each login.
+
+
+ +
+

Note! LDAP authentication works by binding to the directory with a service account to locate the user, then re-binding as that user to validate credentials.

+

Note! If a user exists locally with a password, local authentication takes priority. LDAP is used when no local account is found, or when the account was previously provisioned via LDAP.

+

Note! LDAP users cannot use Two-Factor Authentication (TOTP). Credentials are validated by the LDAP directory on each login.

+

Note! It is recommended to always keep a local administrator account as a fallback in case the LDAP server becomes unreachable.

+

Note! The following environment variables can be used to configure LDAP: DNS_SERVER_LDAP_ENABLED, DNS_SERVER_LDAP_SERVER, DNS_SERVER_LDAP_PORT, DNS_SERVER_LDAP_USE_SSL, DNS_SERVER_LDAP_BIND_DN, DNS_SERVER_LDAP_BIND_PASSWORD, DNS_SERVER_LDAP_SEARCH_BASE, DNS_SERVER_LDAP_USER_FILTER, DNS_SERVER_LDAP_GROUP_ATTRIBUTE, DNS_SERVER_LDAP_ALLOW_SIGNUP, DNS_SERVER_LDAP_ALLOW_SIGNUP_ONLY_FOR_MAPPED_USERS, DNS_SERVER_LDAP_GROUP_MAP.

+
+
+ +
+ + +
+
+
+
diff --git a/DnsServerCore/www/js/auth.js b/DnsServerCore/www/js/auth.js index 8c1325d4..2968dfd2 100644 --- a/DnsServerCore/www/js/auth.js +++ b/DnsServerCore/www/js/auth.js @@ -655,7 +655,8 @@ function showMyProfileModal() { $("#mnuUserDisplayName").text(sessionData.displayName); - $("#txtMyProfileDisplayName").prop("disabled", responseJSON.response.isSsoUser); + var isRemoteUser = responseJSON.response.isSsoUser || responseJSON.response.isLdapUser; + $("#txtMyProfileDisplayName").prop("disabled", isRemoteUser); $("#txtMyProfileDisplayName").val(responseJSON.response.displayName); $("#txtMyProfileUsername").val(responseJSON.response.username); @@ -663,6 +664,10 @@ function showMyProfileModal() { $("#lblMyProfileUserType").text("Remote/SSO"); $("#lblMyProfile2FAStatus").text("SSO Managed"); } + else if (responseJSON.response.isLdapUser) { + $("#lblMyProfileUserType").text("Remote/LDAP"); + $("#lblMyProfile2FAStatus").text("LDAP Managed"); + } else { $("#lblMyProfileUserType").text("Local"); $("#lblMyProfile2FAStatus").text(responseJSON.response.totpEnabled ? "Enabled" : "Disabled"); @@ -842,6 +847,8 @@ function refreshAdminTab() { refreshAdminPermissions(); else if ($("#adminTabListSso").hasClass("active")) refreshAdminSsoConfig(); + else if ($("#adminTabListLdap").hasClass("active")) + refreshAdminLdapConfig(); else if ($("#adminTabListCluster").hasClass("active")) refreshAdminCluster(); else @@ -1113,6 +1120,10 @@ function getAdminUsersRowHtml(id, user) { userType = "Remote/SSO"; totpStatus = "SSO Managed" } + else if (user.isLdapUser) { + userType = "Remote/LDAP"; + totpStatus = "LDAP Managed" + } else { userType = "Local"; @@ -1256,16 +1267,22 @@ function showUserDetailsModal(objMenuItem) { url: "api/admin/users/get?user=" + encodeURIComponent(username) + "&includeGroups=true", token: sessionData.token, success: function (responseJSON) { - $("#txtUserDetailsDisplayName").prop("disabled", responseJSON.response.isSsoUser); + var isRemoteUser = responseJSON.response.isSsoUser || responseJSON.response.isLdapUser; + + $("#txtUserDetailsDisplayName").prop("disabled", isRemoteUser); $("#txtUserDetailsDisplayName").val(responseJSON.response.displayName); - $("#txtUserDetailsUsername").prop("disabled", responseJSON.response.isSsoUser); + $("#txtUserDetailsUsername").prop("disabled", isRemoteUser); $("#txtUserDetailsUsername").val(responseJSON.response.username); if (responseJSON.response.isSsoUser) { $("#lblUserDetailsUserType").text("Remote/SSO"); $("#lblUserDetails2FAStatus").text("SSO Managed"); } + else if (responseJSON.response.isLdapUser) { + $("#lblUserDetailsUserType").text("Remote/LDAP"); + $("#lblUserDetails2FAStatus").text("LDAP Managed"); + } else { $("#lblUserDetailsUserType").text("Local"); $("#lblUserDetails2FAStatus").text(responseJSON.response.totpEnabled ? "Enabled" : "Disabled"); @@ -1280,8 +1297,10 @@ function showUserDetailsModal(objMenuItem) { memberOf += htmlEncode(responseJSON.response.memberOfGroups[i]) + "\n"; } - $("#txtUserDetailsMemberOf").prop("disabled", responseJSON.response.isSsoUser && responseJSON.response.ssoManagedGroups) - $("#optUserDetailsGroupList").prop("disabled", responseJSON.response.isSsoUser && responseJSON.response.ssoManagedGroups) + var groupsDisabled = (responseJSON.response.isSsoUser && responseJSON.response.ssoManagedGroups) || + (responseJSON.response.isLdapUser && responseJSON.response.ldapManagedGroups); + $("#txtUserDetailsMemberOf").prop("disabled", groupsDisabled) + $("#optUserDetailsGroupList").prop("disabled", groupsDisabled) $("#txtUserDetailsMemberOf").val(memberOf); @@ -2282,3 +2301,168 @@ function saveAdminSsoConfig(objBtn) { } }); } + +function refreshAdminLdapConfig() { + var divAdminLdapLoader = $("#divAdminLdapLoader"); + var divAdminLdapView = $("#divAdminLdapView"); + + divAdminLdapLoader.show(); + divAdminLdapView.hide(); + + HTTPRequest({ + url: "api/admin/ldap/get?includeGroups=true", + token: sessionData.token, + success: function (responseJSON) { + localGroups = responseJSON.response.localGroups; + + loadAdminLdapConfig(responseJSON); + + divAdminLdapLoader.hide(); + divAdminLdapView.show(); + }, + invalidToken: function () { + showPageLogin(); + }, + objLoaderPlaceholder: divAdminLdapLoader + }); +} + +function loadAdminLdapConfig(responseJSON) { + $("#chkAdminLdapEnabled").prop("checked", responseJSON.response.ldapEnabled); + $("#txtAdminLdapServer").val(responseJSON.response.ldapServer || ""); + $("#txtAdminLdapPort").val(responseJSON.response.ldapPort || 389); + $("#chkAdminLdapUseSsl").prop("checked", responseJSON.response.ldapUseSsl); + $("#chkAdminLdapIgnoreSslErrors").prop("checked", responseJSON.response.ldapIgnoreSslErrors); + $("#txtAdminLdapBindDn").val(responseJSON.response.ldapBindDn || ""); + $("#txtAdminLdapBindPassword").val(responseJSON.response.ldapBindPassword || ""); + $("#txtAdminLdapSearchBase").val(responseJSON.response.ldapSearchBase || ""); + $("#txtAdminLdapUserFilter").val(responseJSON.response.ldapUserFilter || ""); + $("#txtAdminLdapGroupAttribute").val(responseJSON.response.ldapGroupAttribute || ""); + $("#chkAdminLdapAllowSignup").prop("checked", responseJSON.response.ldapAllowSignup); + $("#chkAdminLdapAllowSignupOnlyForMappedUsers").prop("checked", responseJSON.response.ldapAllowSignupOnlyForMappedUsers); + + $("#tableAdminLdapGroupMap").html(""); + + for (var i = 0; i < responseJSON.response.ldapGroupMap.length; i++) + addAdminLdapGroupMapRow(responseJSON.response.ldapGroupMap[i].remoteGroup, responseJSON.response.ldapGroupMap[i].localGroup); +} + +function addAdminLdapGroupMapRow(remoteGroup, localGroup) { + var id = Math.floor(Math.random() * 10000); + + var tableHtmlRows = ""; + + tableHtmlRows += ""; + + tableHtmlRows += ""; + + $("#tableAdminLdapGroupMap").append(tableHtmlRows); +} + +function saveAdminLdapConfig(objBtn) { + var btn = $(objBtn); + + var ldapEnabled = $("#chkAdminLdapEnabled").prop("checked"); + + var ldapServer = $("#txtAdminLdapServer").val(); + if (ldapEnabled && (ldapServer === "")) { + showAlert("warning", "Missing!", "Please enter the LDAP Server address."); + $("#txtAdminLdapServer").trigger("focus"); + return; + } + + var ldapPort = $("#txtAdminLdapPort").val(); + if (ldapEnabled && (ldapPort === "")) { + showAlert("warning", "Missing!", "Please enter the LDAP Port."); + $("#txtAdminLdapPort").trigger("focus"); + return; + } + + var ldapUseSsl = $("#chkAdminLdapUseSsl").prop("checked"); + var ldapIgnoreSslErrors = $("#chkAdminLdapIgnoreSslErrors").prop("checked"); + var ldapBindDn = $("#txtAdminLdapBindDn").val(); + var ldapBindPassword = $("#txtAdminLdapBindPassword").val(); + var ldapSearchBase = $("#txtAdminLdapSearchBase").val(); + var ldapUserFilter = $("#txtAdminLdapUserFilter").val(); + var ldapGroupAttribute = $("#txtAdminLdapGroupAttribute").val(); + var ldapAllowSignup = $("#chkAdminLdapAllowSignup").prop("checked"); + var ldapAllowSignupOnlyForMappedUsers = $("#chkAdminLdapAllowSignupOnlyForMappedUsers").prop("checked"); + + var ldapGroupMap = serializeTableData($("#tableAdminLdapGroupMap"), 2); + if (ldapGroupMap === false) + return; + + if (ldapGroupMap.length == 0) + ldapGroupMap = false; + + btn.button("loading"); + + HTTPRequest({ + url: "api/admin/ldap/set", + token: sessionData.token, + method: "POST", + data: "ldapEnabled=" + ldapEnabled + "&ldapServer=" + encodeURIComponent(ldapServer) + "&ldapPort=" + ldapPort + "&ldapUseSsl=" + ldapUseSsl + "&ldapIgnoreSslErrors=" + ldapIgnoreSslErrors + "&ldapBindDn=" + encodeURIComponent(ldapBindDn) + "&ldapBindPassword=" + encodeURIComponent(ldapBindPassword) + "&ldapSearchBase=" + encodeURIComponent(ldapSearchBase) + "&ldapUserFilter=" + encodeURIComponent(ldapUserFilter) + "&ldapGroupAttribute=" + encodeURIComponent(ldapGroupAttribute) + "&ldapAllowSignup=" + ldapAllowSignup + "&ldapAllowSignupOnlyForMappedUsers=" + ldapAllowSignupOnlyForMappedUsers + "&ldapGroupMap=" + encodeURIComponent(ldapGroupMap), + success: function (responseJSON) { + loadAdminLdapConfig(responseJSON); + btn.button("reset"); + + showAlert("success", "LDAP Config Saved!", "LDAP authentication config was saved successfully."); + }, + error: function () { + btn.button("reset"); + }, + invalidToken: function () { + btn.button("reset"); + showPageLogin(); + } + }); +} + +function testAdminLdapConnection(objBtn) { + var btn = $(objBtn); + + var ldapServer = $("#txtAdminLdapServer").val(); + if (ldapServer === "") { + showAlert("warning", "Missing!", "Please enter the LDAP Server address."); + $("#txtAdminLdapServer").trigger("focus"); + return; + } + + var ldapPort = $("#txtAdminLdapPort").val() || 389; + var ldapUseSsl = $("#chkAdminLdapUseSsl").prop("checked"); + var ldapIgnoreSslErrors = $("#chkAdminLdapIgnoreSslErrors").prop("checked"); + var ldapBindDn = $("#txtAdminLdapBindDn").val(); + var ldapBindPassword = $("#txtAdminLdapBindPassword").val(); + var ldapSearchBase = $("#txtAdminLdapSearchBase").val(); + var ldapUserFilter = $("#txtAdminLdapUserFilter").val(); + var ldapGroupAttribute = $("#txtAdminLdapGroupAttribute").val(); + + btn.button("loading"); + + HTTPRequest({ + url: "api/admin/ldap/test", + token: sessionData.token, + method: "POST", + data: "ldapServer=" + encodeURIComponent(ldapServer) + "&ldapPort=" + ldapPort + "&ldapUseSsl=" + ldapUseSsl + "&ldapIgnoreSslErrors=" + ldapIgnoreSslErrors + "&ldapBindDn=" + encodeURIComponent(ldapBindDn) + "&ldapBindPassword=" + encodeURIComponent(ldapBindPassword) + "&ldapSearchBase=" + encodeURIComponent(ldapSearchBase) + "&ldapUserFilter=" + encodeURIComponent(ldapUserFilter) + "&ldapGroupAttribute=" + encodeURIComponent(ldapGroupAttribute), + success: function (responseJSON) { + btn.button("reset"); + + if (responseJSON.response.success) + showAlert("success", "Connection Successful!", responseJSON.response.message); + else + showAlert("danger", "Connection Failed!", responseJSON.response.message); + }, + error: function () { + btn.button("reset"); + }, + invalidToken: function () { + btn.button("reset"); + showPageLogin(); + } + }); +}