From 294ee567f6cdfac1b1b89c243a6a4aee4a53a8e1 Mon Sep 17 00:00:00 2001 From: Roy Hagland Date: Tue, 28 Apr 2026 13:28:04 +0200 Subject: [PATCH] Add LDAP/Active Directory authentication support Adds native LDAP SSO authentication allowing users to sign in with their Active Directory or LDAP directory credentials. Supports plain LDAP, StartTLS (port 389), and LDAPS (port 636). Includes group-to- role mapping, configurable auto-provisioning, and full environment variable support for Docker deployments. --- DnsServerCore/Auth/AuthManager.cs | 453 ++++++++++++++++++++++++- DnsServerCore/Auth/LdapAuthProvider.cs | 251 ++++++++++++++ DnsServerCore/Auth/User.cs | 93 +++-- DnsServerCore/DnsServerCore.csproj | 1 + DnsServerCore/DnsWebService.cs | 4 + DnsServerCore/WebServiceAuthApi.cs | 176 +++++++++- DnsServerCore/www/index.html | 144 ++++++++ DnsServerCore/www/js/auth.js | 194 ++++++++++- 8 files changed, 1288 insertions(+), 28 deletions(-) create mode 100644 DnsServerCore/Auth/LdapAuthProvider.cs diff --git a/DnsServerCore/Auth/AuthManager.cs b/DnsServerCore/Auth/AuthManager.cs index ae7023134..58c97544a 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 000000000..fdeda0fea --- /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 4fa4731bf..6e940c875 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 6d710843e..f0a6e2e2c 100644 --- a/DnsServerCore/DnsServerCore.csproj +++ b/DnsServerCore/DnsServerCore.csproj @@ -47,6 +47,7 @@ + diff --git a/DnsServerCore/DnsWebService.cs b/DnsServerCore/DnsWebService.cs index df5ad6ca8..623015a35 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 b329274fa..2e3002892 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 9a76064aa..ea458be27 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 8c1325d46..2968dfd24 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(); + } + }); +}