/* * * (c) Copyright Ascensio System Limited 2010-2021 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // ReSharper disable RedundantToStringCall using Constants = ASC.Core.Users.Constants; namespace ASC.ActiveDirectory.Base; [Scope] public class LdapUserImporter : IDisposable { public List AllDomainUsers { get; private set; } public List AllDomainGroups { get; private set; } public Dictionary AllSkipedDomainUsers { get; private set; } public Dictionary AllSkipedDomainGroups { get; private set; } private string _ldapDomain; private readonly string _unknownDomain; public string LDAPDomain { get { if (!string.IsNullOrEmpty(_ldapDomain)) return _ldapDomain; _ldapDomain = LoadLDAPDomain(); if (string.IsNullOrEmpty(_ldapDomain)) { _ldapDomain = _unknownDomain; } return _ldapDomain; } } public List PrimaryGroupIds { get; set; } public LdapSettings Settings { get { return LdapHelper.Settings; } } public LdapHelper LdapHelper { get; private set; } public LdapLocalization Resource { get; private set; } private List _watchedNestedGroups; private readonly ILog _log; private readonly LdapObjectExtension _ldapObjectExtension; private UserManager UserManager { get; set; } public LdapUserImporter( IOptionsMonitor option, UserManager userManager, IConfiguration configuration, NovellLdapHelper novellLdapHelper, LdapObjectExtension ldapObjectExtension) { _unknownDomain = configuration["ldap:domain"] ?? "LDAP"; AllDomainUsers = new List(); AllDomainGroups = new List(); AllSkipedDomainUsers = new Dictionary(); AllSkipedDomainGroups = new Dictionary(); LdapHelper = novellLdapHelper; _log = option.Get("ASC"); UserManager = userManager; _watchedNestedGroups = new List(); _ldapObjectExtension = ldapObjectExtension; } public void Init(LdapSettings settings, LdapLocalization resource) { LdapHelper.Init(settings); Resource = resource; } public List GetDiscoveredUsersByAttributes() { var users = new List(); if (!AllDomainUsers.Any() && !TryLoadLDAPUsers()) return users; var usersToAdd = AllDomainUsers.Select(ldapObject => _ldapObjectExtension.ToUserInfo(ldapObject, this, _log)); users.AddRange(usersToAdd); return users; } public List GetDiscoveredGroupsByAttributes() { if (!Settings.GroupMembership) return new List(); if (!AllDomainGroups.Any() && !TryLoadLDAPGroups()) return new List(); var groups = new List(); var groupsToAdd = AllDomainGroups.ConvertAll(g => LdapObjectExtension.ToGroupInfo(g, Settings)); groups.AddRange(groupsToAdd); return groups; } public List GetGroupUsers(GroupInfo groupInfo) { return GetGroupUsers(groupInfo, true); } private List GetGroupUsers(GroupInfo groupInfo, bool clearCache) { if (!LdapHelper.IsConnected) LdapHelper.Connect(); _log.DebugFormat("LdapUserImporter.GetGroupUsers(Group name: {0})", groupInfo.Name); var users = new List(); if (!AllDomainGroups.Any() && !TryLoadLDAPGroups()) return users; var domainGroup = AllDomainGroups.FirstOrDefault(lg => lg.Sid.Equals(groupInfo.Sid)); if (domainGroup == null) return users; var members = LdapObjectExtension.GetAttributes(domainGroup, Settings.GroupAttribute, _log); foreach (var member in members) { var ldapUser = FindUserByMember(member); if (ldapUser == null) { var nestedLdapGroup = FindGroupByMember(member); if (nestedLdapGroup != null) { _log.DebugFormat("Found nested LDAP Group: {0}", nestedLdapGroup.DistinguishedName); if (clearCache) _watchedNestedGroups = new List(); if (_watchedNestedGroups.Contains(nestedLdapGroup.DistinguishedName)) { _log.DebugFormat("Skip already watched nested LDAP Group: {0}", nestedLdapGroup.DistinguishedName); continue; } _watchedNestedGroups.Add(nestedLdapGroup.DistinguishedName); var nestedGroupInfo = LdapObjectExtension.ToGroupInfo(nestedLdapGroup, Settings, _log); var nestedGroupUsers = GetGroupUsers(nestedGroupInfo, false); foreach (var groupUser in nestedGroupUsers) { if (!users.Exists(u => u.Sid == groupUser.Sid)) users.Add(groupUser); } } continue; } var userInfo = _ldapObjectExtension.ToUserInfo(ldapUser, this, _log); if (!users.Exists(u => u.Sid == userInfo.Sid)) users.Add(userInfo); } if (PrimaryGroupIds != null && PrimaryGroupIds.Any(id => domainGroup.Sid.EndsWith("-" + id))) { // Domain Users found var ldapUsers = FindUsersByPrimaryGroup(domainGroup.Sid); foreach (var ldapUser in ldapUsers) { var userInfo = _ldapObjectExtension.ToUserInfo(ldapUser, this, _log); if (!users.Exists(u => u.Sid == userInfo.Sid)) users.Add(userInfo); } } return users; } const string GROUP_MEMBERSHIP = "groupMembership"; private IEnumerable GetLdapUserGroups(LdapObject ldapUser) { var ldapUserGroups = new List(); try { if (!Settings.GroupMembership) { return ldapUserGroups; } if (ldapUser == null || string.IsNullOrEmpty(ldapUser.Sid)) { return ldapUserGroups; } if (!LdapHelper.IsConnected) LdapHelper.Connect(); var userGroups = LdapObjectExtension.GetAttributes(ldapUser, LdapConstants.ADSchemaAttributes.MEMBER_OF, _log) .Select(s => LdapUtils.UnescapeLdapString(s)) .ToList(); if (!userGroups.Any()) { userGroups = LdapObjectExtension.GetAttributes(ldapUser, GROUP_MEMBERSHIP, _log); } var searchExpressions = new List(); var primaryGroupId = ldapUser.GetValue(LdapConstants.ADSchemaAttributes.PRIMARY_GROUP_ID) as string; if (!string.IsNullOrEmpty(primaryGroupId)) { var userSid = ldapUser.Sid; var index = userSid.LastIndexOf("-", StringComparison.InvariantCultureIgnoreCase); if (index > -1) { var primaryGroupSid = userSid.Substring(0, index + 1) + primaryGroupId; searchExpressions.Add(Expression.Equal(ldapUser.SidAttribute, primaryGroupSid)); } } if (userGroups.Any()) { var cnRegex = new Regex(",[A-z]{2}="); searchExpressions.AddRange(userGroups .Select(g => g.Substring(0, cnRegex.Match(g).Index)) .Where(s => !string.IsNullOrEmpty(s)) .Select(Expression.Parse) .Where(e => e != null)); var criteria = Criteria.Any(searchExpressions.ToArray()); var foundList = LdapHelper.GetGroups(criteria); if (foundList.Any()) { ldapUserGroups.AddRange(foundList); } } else { var ldapGroups = LdapHelper.GetGroups(); ldapUserGroups.AddRange( ldapGroups.Where( ldapGroup => LdapHelper.UserExistsInGroup(ldapGroup, ldapUser, Settings))); } } catch (Exception ex) { if (ldapUser != null) _log.ErrorFormat("IsUserExistInGroups(login: '{0}' sid: '{1}') error {2}", ldapUser.DistinguishedName, ldapUser.Sid, ex); } return ldapUserGroups; } public IEnumerable GetAndCheckCurrentGroups(LdapObject ldapUser, IEnumerable portalGroups) { var result = new List(); try { var searchExpressions = new List(); if (portalGroups != null && portalGroups.Any()) { searchExpressions.AddRange(portalGroups.Select(g => Expression.Equal(LdapConstants.ADSchemaAttributes.OBJECT_SID, g.Sid))); } else { return result; } var criteria = Criteria.Any(searchExpressions.ToArray()); var foundList = LdapHelper.GetGroups(criteria); if (foundList.Any()) { var stillExistingGroups = portalGroups.Where(g => foundList.Any(fg => fg.Sid == g.Sid)); foreach (var group in stillExistingGroups) { if (GetGroupUsers(group).Any(u => u.Sid == ldapUser.Sid)) { result.Add(group); } } } } catch (Exception ex) { if (ldapUser != null) _log.ErrorFormat("GetAndCheckCurrentGroups(login: '{0}' sid: '{1}') error {2}", ldapUser.DistinguishedName, ldapUser.Sid, ex); } return result; } public bool TrySyncUserGroupMembership(Tuple ldapUserInfo) { if (ldapUserInfo == null || !Settings.GroupMembership) { return false; } var userInfo = ldapUserInfo.Item1; var ldapUser = ldapUserInfo.Item2; var portalUserLdapGroups = UserManager.GetUserGroups(userInfo.ID, IncludeType.All) .Where(g => !string.IsNullOrEmpty(g.Sid)) .ToList(); List ldapUserGroupList = new List(); ldapUserGroupList.AddRange(GetLdapUserGroups(ldapUser)); if (!LdapHelper.IsConnected) LdapHelper.Connect(); var actualPortalLdapGroups = GetAndCheckCurrentGroups(ldapUser, portalUserLdapGroups).ToList(); foreach (var ldapUserGroup in ldapUserGroupList) { var groupInfo = UserManager.GetGroupInfoBySid(ldapUserGroup.Sid); if (Equals(groupInfo, Constants.LostGroupInfo)) { _log.DebugFormat("TrySyncUserGroupMembership(groupname: '{0}' sid: '{1}') no portal group found, creating", ldapUserGroup.DistinguishedName, ldapUserGroup.Sid); groupInfo = UserManager.SaveGroupInfo(LdapObjectExtension.ToGroupInfo(ldapUserGroup, Settings, _log)); _log.DebugFormat("TrySyncUserGroupMembership(username: '{0}' sid: '{1}') adding user to group (groupname: '{2}' sid: '{3}')", userInfo.UserName, ldapUser.Sid, groupInfo.Name, groupInfo.Sid); UserManager.AddUserIntoGroup(userInfo.ID, groupInfo.ID); } else if (!portalUserLdapGroups.Contains(groupInfo)) { _log.DebugFormat("TrySyncUserGroupMembership(username: '{0}' sid: '{1}') adding user to group (groupname: '{2}' sid: '{3}')", userInfo.UserName, ldapUser.Sid, groupInfo.Name, groupInfo.Sid); UserManager.AddUserIntoGroup(userInfo.ID, groupInfo.ID); } actualPortalLdapGroups.Add(groupInfo); } foreach (var portalUserLdapGroup in portalUserLdapGroups) { if (!actualPortalLdapGroups.Contains(portalUserLdapGroup)) { _log.DebugFormat("TrySyncUserGroupMembership(username: '{0}' sid: '{1}') removing user from group (groupname: '{2}' sid: '{3}')", userInfo.UserName, ldapUser.Sid, portalUserLdapGroup.Name, portalUserLdapGroup.Sid); UserManager.RemoveUserFromGroup(userInfo.ID, portalUserLdapGroup.ID); } } return actualPortalLdapGroups.Count != 0; } public bool TryLoadLDAPUsers() { try { if (!Settings.EnableLdapAuthentication) return false; if (!LdapHelper.IsConnected) LdapHelper.Connect(); var users = LdapHelper.GetUsers(); foreach (var user in users) { if (string.IsNullOrEmpty(user.Sid)) { AllSkipedDomainUsers.Add(user, LdapSettingsStatus.WrongSidAttribute); continue; } if (!CheckLoginAttribute(user, Settings.LoginAttribute)) { AllSkipedDomainUsers.Add(user, LdapSettingsStatus.WrongLoginAttribute); continue; } if (!Settings.GroupMembership) { AllDomainUsers.Add(user); continue; } if (!Settings.UserAttribute.Equals(LdapConstants.RfcLDAPAttributes.DN, StringComparison.InvariantCultureIgnoreCase) && !CheckUserAttribute(user, Settings.UserAttribute)) { AllSkipedDomainUsers.Add(user, LdapSettingsStatus.WrongUserAttribute); continue; } AllDomainUsers.Add(user); } if (AllDomainUsers.Any()) { PrimaryGroupIds = AllDomainUsers.Select(u => u.GetValue(LdapConstants.ADSchemaAttributes.PRIMARY_GROUP_ID)).Cast() .Distinct().ToList(); } return AllDomainUsers.Any() || !users.Any(); } catch (ArgumentException) { _log.ErrorFormat("TryLoadLDAPUsers(): Incorrect filter. userFilter = {0}", Settings.UserFilter); } return false; } public bool TryLoadLDAPGroups() { try { if (!Settings.EnableLdapAuthentication || !Settings.GroupMembership) { return false; } if (!LdapHelper.IsConnected) LdapHelper.Connect(); var groups = LdapHelper.GetGroups(); foreach (var group in groups) { if (string.IsNullOrEmpty(group.Sid)) { AllSkipedDomainGroups.Add(group, LdapSettingsStatus.WrongSidAttribute); continue; } if (!CheckGroupAttribute(group, Settings.GroupAttribute)) { AllSkipedDomainGroups.Add(group, LdapSettingsStatus.WrongGroupAttribute); continue; } if (!CheckGroupNameAttribute(group, Settings.GroupNameAttribute)) { AllSkipedDomainGroups.Add(group, LdapSettingsStatus.WrongGroupNameAttribute); continue; } AllDomainGroups.Add(group); } return AllDomainGroups.Any() || !groups.Any(); } catch (ArgumentException) { _log.ErrorFormat("TryLoadLDAPGroups(): Incorrect group filter. groupFilter = {0}", Settings.GroupFilter); } return false; } private string LoadLDAPDomain() { try { if (!Settings.EnableLdapAuthentication) return null; if (!LdapHelper.IsConnected) LdapHelper.Connect(); string ldapDomain; if (AllDomainUsers.Any()) { ldapDomain = LdapObjectExtension.GetDomainFromDn(AllDomainUsers.First()); if (!string.IsNullOrEmpty(ldapDomain)) return ldapDomain; } ldapDomain = LdapHelper.SearchDomain(); if (!string.IsNullOrEmpty(ldapDomain)) return ldapDomain; ldapDomain = LdapUtils.DistinguishedNameToDomain(Settings.UserDN); if (!string.IsNullOrEmpty(ldapDomain)) return ldapDomain; ldapDomain = LdapUtils.DistinguishedNameToDomain(Settings.GroupDN); if (!string.IsNullOrEmpty(ldapDomain)) return ldapDomain; } catch (Exception ex) { _log.ErrorFormat("LoadLDAPDomain(): Error: {0}", ex); } return null; } protected bool CheckLoginAttribute(LdapObject user, string loginAttribute) { try { var member = user.GetValue(loginAttribute); if (member == null || string.IsNullOrWhiteSpace(member.ToString())) { _log.DebugFormat("Login Attribute parameter ({0}) not found: DN = {1}", Settings.LoginAttribute, user.DistinguishedName); return false; } } catch (Exception e) { _log.ErrorFormat("Login Attribute parameter ({0}) not found: loginAttribute = {1}. {2}", Settings.LoginAttribute, loginAttribute, e); return false; } return true; } protected bool CheckUserAttribute(LdapObject user, string userAttr) { try { var userAttribute = user.GetValue(userAttr); if (userAttribute == null || string.IsNullOrWhiteSpace(userAttribute.ToString())) { _log.DebugFormat("User Attribute parameter ({0}) not found: DN = {1}", Settings.UserAttribute, user.DistinguishedName); return false; } } catch (Exception e) { _log.ErrorFormat("User Attribute parameter ({0}) not found: userAttr = {1}. {2}", Settings.UserAttribute, userAttr, e); return false; } return true; } protected bool CheckGroupAttribute(LdapObject group, string groupAttr) { try { group.GetValue(groupAttr); // Group attribute can be empty - example => Domain users } catch (Exception e) { _log.ErrorFormat("Group Attribute parameter ({0}) not found: {1}. {2}", Settings.GroupAttribute, groupAttr, e); return false; } return true; } protected bool CheckGroupNameAttribute(LdapObject group, string groupAttr) { try { var groupNameAttribute = group.GetValues(groupAttr); if (!groupNameAttribute.Any()) { _log.DebugFormat("Group Name Attribute parameter ({0}) not found: {1}", Settings.GroupNameAttribute, groupAttr); return false; } } catch (Exception e) { _log.ErrorFormat("Group Attribute parameter ({0}) not found: {1}. {2}", Settings.GroupNameAttribute, groupAttr, e); return false; } return true; } private List FindUsersByPrimaryGroup(string sid) { _log.Debug("LdapUserImporter.FindUsersByPrimaryGroup()"); if (!AllDomainUsers.Any() && !TryLoadLDAPUsers()) return null; return AllDomainUsers.Where( lu => { var primaryGroupId = lu.GetValue(LdapConstants.ADSchemaAttributes.PRIMARY_GROUP_ID) as string; return !string.IsNullOrEmpty(primaryGroupId) && sid.EndsWith(primaryGroupId); }) .ToList(); } private LdapObject FindUserByMember(string userAttributeValue) { if (!AllDomainUsers.Any() && !TryLoadLDAPUsers()) return null; _log.DebugFormat("LdapUserImporter.FindUserByMember(user attr: {0})", userAttributeValue); return AllDomainUsers.FirstOrDefault(u => u.DistinguishedName.Equals(userAttributeValue, StringComparison.InvariantCultureIgnoreCase) || Convert.ToString(u.GetValue(Settings.UserAttribute)).Equals(userAttributeValue, StringComparison.InvariantCultureIgnoreCase)); } private LdapObject FindGroupByMember(string member) { if (!AllDomainGroups.Any() && !TryLoadLDAPGroups()) return null; _log.DebugFormat("LdapUserImporter.FindGroupByMember(member: {0})", member); return AllDomainGroups.FirstOrDefault(g => g.DistinguishedName.Equals(member, StringComparison.InvariantCultureIgnoreCase)); } public List> FindLdapUsers(string login) { var listResults = new List>(); var ldapLogin = LdapLogin.ParseLogin(login); if (ldapLogin == null) return listResults; if (!LdapHelper.IsConnected) LdapHelper.Connect(); var exps = new List { Expression.Equal(Settings.LoginAttribute, ldapLogin.Username) }; if (!ldapLogin.Username.Equals(login) && ldapLogin.ToString().Equals(login)) { exps.Add(Expression.Equal(Settings.LoginAttribute, login)); } string email = null; if (!string.IsNullOrEmpty(Settings.MailAttribute) && !string.IsNullOrEmpty(ldapLogin.Domain) && login.Contains("@")) { email = ldapLogin.ToString(); exps.Add(Expression.Equal(Settings.MailAttribute, email)); } var searchTerm = exps.Count > 1 ? Criteria.Any(exps.ToArray()).ToString() : exps.First().ToString(); var users = LdapHelper.GetUsers(searchTerm, !string.IsNullOrEmpty(email) ? -1 : 1) .Where(user => user != null) .ToLookup(lu => { var ui = Constants.LostUser; try { if (string.IsNullOrEmpty(_ldapDomain)) { _ldapDomain = LdapUtils.DistinguishedNameToDomain(lu.DistinguishedName); } ui = _ldapObjectExtension.ToUserInfo(lu, this, _log); } catch (Exception ex) { _log.ErrorFormat("FindLdapUser->ToUserInfo() failed. Error: {0}", ex.ToString()); } return Tuple.Create(ui, lu); }); if (!users.Any()) return listResults; foreach (var user in users) { var ui = user.Key.Item1; if (ui.Equals(Constants.LostUser)) continue; var ul = user.Key.Item2; var ldapLoginAttribute = ul.GetValue(Settings.LoginAttribute) as string; if (string.IsNullOrEmpty(ldapLoginAttribute)) { _log.WarnFormat("LDAP: DN: '{0}' Login Attribute '{1}' is empty", ul.DistinguishedName, Settings.LoginAttribute); continue; } if (ldapLoginAttribute.Equals(login)) { listResults.Add(user.Key); continue; } if (!string.IsNullOrEmpty(email)) { if (ui.Email.Equals(email, StringComparison.InvariantCultureIgnoreCase)) { listResults.Add(user.Key); continue; } } if (LdapUtils.IsLoginAccepted(ldapLogin, ui, LDAPDomain)) { listResults.Add(user.Key); } } return listResults; } public List FindUsersByAttribute(string key, string value, StringComparison comparison = StringComparison.InvariantCultureIgnoreCase) { var users = new List(); if (!AllDomainUsers.Any() && !TryLoadLDAPUsers()) return users; return users.Where(us => !us.IsDisabled && string.Equals((string)us.GetValue(key), value, comparison)).ToList(); } public List FindUsersByAttribute(string key, IEnumerable value, StringComparison comparison = StringComparison.InvariantCultureIgnoreCase) { var users = new List(); if (!AllDomainUsers.Any() && !TryLoadLDAPUsers()) return users; return AllDomainUsers.Where(us => !us.IsDisabled && value.Any(val => string.Equals(val, (string)us.GetValue(key), comparison))).ToList(); } public List FindGroupsByAttribute(string key, string value, StringComparison comparison = StringComparison.InvariantCultureIgnoreCase) { var gr = new List(); if (!AllDomainGroups.Any() && !TryLoadLDAPGroups()) return gr; return gr.Where(g => !g.IsDisabled && string.Equals((string)g.GetValue(key), value, comparison)).ToList(); } public List FindGroupsByAttribute(string key, IEnumerable value, StringComparison comparison = StringComparison.InvariantCultureIgnoreCase) { var gr = new List(); if (!AllDomainGroups.Any() && !TryLoadLDAPGroups()) return gr; return AllDomainGroups.Where(g => !g.IsDisabled && value.Any(val => string.Equals(val, (string)g.GetValue(key), comparison))).ToList(); } public Tuple Login(string login, string password) { try { if (string.IsNullOrEmpty(login) || string.IsNullOrEmpty(password)) return null; var ldapUsers = FindLdapUsers(login); _log.DebugFormat("FindLdapUsers(login '{0}') found: {1} users", login, ldapUsers.Count); foreach (var ldapUser in ldapUsers) { string currentLogin = null; try { var ldapUserInfo = ldapUser.Item1; var ldapUserObject = ldapUser.Item2; if (ldapUserInfo.Equals(Constants.LostUser) || ldapUserObject == null) { continue; } else if (string.IsNullOrEmpty(ldapUserObject.DistinguishedName) || string.IsNullOrEmpty(ldapUserObject.Sid)) { _log.DebugFormat("LdapUserImporter->Login(login: '{0}', dn: '{1}') failed. Error: missing DN or SID", login, ldapUserObject.Sid); continue; } currentLogin = ldapUserObject.DistinguishedName; _log.DebugFormat("LdapUserImporter.Login('{0}')", currentLogin); LdapHelper.CheckCredentials(currentLogin, password, Settings.Server, Settings.PortNumber, Settings.StartTls, Settings.Ssl, Settings.AcceptCertificate, Settings.AcceptCertificateHash); return new Tuple(ldapUserInfo, ldapUserObject); } catch (Exception ex) { _log.ErrorFormat("LdapUserImporter->Login(login: '{0}') failed. Error: {1}", currentLogin ?? login, ex); } } } catch (Exception ex) { _log.ErrorFormat("LdapUserImporter->Login({0}) failed {1}", login, ex); } return null; } public void Dispose() { if (LdapHelper != null) LdapHelper.Dispose(); } }