diff --git a/app/importer/server/classes/ImportDataConverter.ts b/app/importer/server/classes/ImportDataConverter.ts index 9f09a44d49bd6..906ead79effcc 100644 --- a/app/importer/server/classes/ImportDataConverter.ts +++ b/app/importer/server/classes/ImportDataConverter.ts @@ -219,11 +219,18 @@ export class ImportDataConverter { userData._id = _id; + if (!userData.roles && !existingUser.roles) { + userData.roles = ['user']; + } + if (!userData.type && !existingUser.type) { + userData.type = 'user'; + } + // #ToDo: #TODO: Move this to the model class const updateData: Record = { $set: { - roles: userData.roles || ['user'], - type: userData.type || 'user', + ...userData.roles && { roles: userData.roles }, + ...userData.type && { type: userData.type }, ...userData.statusText && { statusText: userData.statusText }, ...userData.bio && { bio: userData.bio }, ...userData.services?.ldap && { ldap: true }, @@ -235,7 +242,13 @@ export class ImportDataConverter { this.addUserServices(updateData, userData); this.addUserImportId(updateData, userData); this.addUserEmails(updateData, userData, existingUser.emails || []); - Users.update({ _id }, updateData); + + if (Object.keys(updateData.$set).length === 0) { + delete updateData.$set; + } + if (Object.keys(updateData).length > 0) { + Users.update({ _id }, updateData); + } if (userData.utcOffset) { Users.setUtcOffset(_id, userData.utcOffset); diff --git a/ee/server/lib/ldap/Manager.ts b/ee/server/lib/ldap/Manager.ts index 5e4c96fca602f..d2f70921dd5ba 100644 --- a/ee/server/lib/ldap/Manager.ts +++ b/ee/server/lib/ldap/Manager.ts @@ -36,17 +36,13 @@ export class LDAPEEManager extends LDAPManager { try { await ldap.connect(); - try { - const createNewUsers = settings.get('LDAP_Background_Sync_Import_New_Users') ?? true; - const updateExistingUsers = settings.get('LDAP_Background_Sync_Keep_Existant_Users_Updated') ?? true; + const createNewUsers = settings.get('LDAP_Background_Sync_Import_New_Users') ?? true; + const updateExistingUsers = settings.get('LDAP_Background_Sync_Keep_Existant_Users_Updated') ?? true; - if (createNewUsers) { - await this.importNewUsers(ldap, converter, updateExistingUsers); - } else if (updateExistingUsers) { - await this.updateExistingUsers(ldap, converter); - } - } finally { - ldap.disconnect(); + if (createNewUsers) { + await this.importNewUsers(ldap, converter, updateExistingUsers); + } else if (updateExistingUsers) { + await this.updateExistingUsers(ldap, converter); } converter.convertUsers({ @@ -55,6 +51,8 @@ export class LDAPEEManager extends LDAPManager { } catch (error) { logger.error(error); } + + ldap.disconnect(); } public static async syncAvatars(): Promise { @@ -116,12 +114,12 @@ export class LDAPEEManager extends LDAPManager { public static async advancedSyncForUser(ldap: LDAPConnection, user: IUser, isNewRecord: boolean, dn: string): Promise { await this.syncUserRoles(ldap, user, dn); await this.syncUserChannels(ldap, user, dn); - await this.syncUserTeams(ldap, user, isNewRecord); + await this.syncUserTeams(ldap, user, dn, isNewRecord); } private static async advancedSync(ldap: LDAPConnection, importUser: IImportUser, converter: LDAPDataConverter, isNewRecord: boolean): Promise { const user = converter.findExistingUser(importUser); - if (!user || user.username) { + if (!user?.username) { return; } @@ -140,6 +138,7 @@ export class LDAPEEManager extends LDAPManager { }; const result = await ldap.searchRaw(baseDN, searchOptions); + if (!Array.isArray(result) || result.length === 0) { logger.debug(`${ username } is not in ${ groupName } group!!!`); } else { @@ -314,7 +313,7 @@ export class LDAPEEManager extends LDAPManager { } } - private static async syncUserTeams(ldap: LDAPConnection, user: IUser, isNewRecord: boolean): Promise { + private static async syncUserTeams(ldap: LDAPConnection, user: IUser, dn: string, isNewRecord: boolean): Promise { if (!user.username) { return; } @@ -324,7 +323,7 @@ export class LDAPEEManager extends LDAPManager { return; } - const ldapUserTeams = await this.getLdapTeamsByUsername(ldap, user.username); + const ldapUserTeams = await this.getLdapTeamsByUsername(ldap, user.username, dn); const mapJson = settings.get('LDAP_Groups_To_Rocket_Chat_Teams'); if (!mapJson) { return; @@ -369,24 +368,42 @@ export class LDAPEEManager extends LDAPManager { return [...new Set(filteredTeams.map((ldapTeam) => mappedTeams[ldapTeam]).flat())]; } - private static async getLdapTeamsByUsername(ldap: LDAPConnection, username: string): Promise> { + private static async getLdapTeamsByUsername(ldap: LDAPConnection, username: string, dn: string): Promise> { + const baseDN = (settings.get('LDAP_Teams_BaseDN') ?? '').trim() || ldap.options.baseDN; const query = settings.get('LDAP_Query_To_Get_User_Teams'); if (!query) { return []; } const searchOptions = { - filter: query.replace(/#{username}/g, username), + filter: query.replace(/#{username}/g, username).replace(/#{userdn}/g, dn), scope: ldap.options.userSearchScope || 'sub', sizeLimit: ldap.options.searchSizeLimit, }; - const ldapUserGroups = await ldap.searchRaw(ldap.options.baseDN, searchOptions); + const attributeNames = (settings.get('LDAP_Teams_Name_Field') ?? '').split(',').map((attributeName) => attributeName.trim()); + if (!attributeNames.length) { + attributeNames.push('ou'); + } + + const ldapUserGroups = await ldap.searchRaw(baseDN, searchOptions); if (!Array.isArray(ldapUserGroups)) { return []; } - return ldapUserGroups.filter((entry) => entry?.raw?.ou).map((entry) => (ldap.extractLdapAttribute(entry.raw.ou) as string)).flat(); + return ldapUserGroups.map((entry) => { + if (!entry?.raw) { + return undefined; + } + + for (const attributeName of attributeNames) { + if (entry.raw[attributeName]) { + return ldap.extractLdapAttribute(entry.raw[attributeName]) as string; + } + } + + return undefined; + }).filter((entry): entry is string => Boolean(entry)).flat(); } private static isUserDeactivated(ldapUser: ILDAPEntry): boolean { diff --git a/ee/server/settings/ldap.ts b/ee/server/settings/ldap.ts index 18517bf6b6205..48b11830fd690 100644 --- a/ee/server/settings/ldap.ts +++ b/ee/server/settings/ldap.ts @@ -200,20 +200,34 @@ export function addSettings(): void { enableQuery: { _id: 'LDAP_Enable', value: true }, invalidValue: false, }); + + const enableQueryTeams = { _id: 'LDAP_Enable_LDAP_Groups_To_RC_Teams', value: true }; + this.add('LDAP_Groups_To_Rocket_Chat_Teams', '{}', { type: 'code', - enableQuery: { _id: 'LDAP_Enable_LDAP_Groups_To_RC_Teams', value: true }, + enableQuery: enableQueryTeams, invalidValue: '{}', }); this.add('LDAP_Validate_Teams_For_Each_Login', false, { type: 'boolean', - enableQuery: { _id: 'LDAP_Enable_LDAP_Groups_To_RC_Teams', value: true }, + enableQuery: enableQueryTeams, invalidValue: false, }); - this.add('LDAP_Query_To_Get_User_Teams', '(&(ou=*)(uniqueMember=uid=#{username},dc=example,dc=com))', { + this.add('LDAP_Teams_BaseDN', '', { + type: 'string', + enableQuery: enableQueryTeams, + invalidValue: '', + }); + this.add('LDAP_Teams_Name_Field', 'ou,cn', { type: 'string', - enableQuery: { _id: 'LDAP_Enable_LDAP_Groups_To_RC_Teams', value: true }, - invalidValue: '(&(ou=*)(uniqueMember=uid=#{username},dc=example,dc=com))', + enableQuery: enableQueryTeams, + invalidValue: '', + }); + + this.add('LDAP_Query_To_Get_User_Teams', '(&(ou=*)(uniqueMember=#{userdn}))', { + type: 'string', + enableQuery: enableQueryTeams, + invalidValue: '', }); }); }); diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index c2e32ce53de47..8f16c2930cd63 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -2552,6 +2552,10 @@ "LDAP_Sync_User_Data_Roles_Filter_Description": "The LDAP search filter used to check if a user is in a group.", "LDAP_Sync_User_Data_RolesMap": "User Data Group Map", "LDAP_Sync_User_Data_RolesMap_Description": "Map LDAP groups to Rocket.Chat user roles
As an example, `{\"rocket-admin\":\"admin\", \"tech-support\":\"support\"}` will map the rocket-admin LDAP group to Rocket's \"admin\" role.", + "LDAP_Teams_BaseDN": "LDAP Teams BaseDN", + "LDAP_Teams_BaseDN_Description": "The LDAP BaseDN used to lookup user teams.", + "LDAP_Teams_Name_Field": "LDAP Team Name Attribute", + "LDAP_Teams_Name_Field_Description": "The LDAP attribute that Rocket.Chat should use to load the team's name. You can specify more than one possible attribute name if you separate them with a comma.", "LDAP_Timeout": "Timeout (ms)", "LDAP_Timeout_Description": "How many mileseconds wait for a search result before return an error", "LDAP_Unique_Identifier_Field": "Unique Identifier Field", diff --git a/server/lib/ldap/Connection.ts b/server/lib/ldap/Connection.ts index 1d4c9130fd22a..c83b8d1393b2b 100644 --- a/server/lib/ldap/Connection.ts +++ b/server/lib/ldap/Connection.ts @@ -278,7 +278,14 @@ export class LDAPConnection { Object.keys(values._raw).forEach((key) => { values[key] = this.extractLdapAttribute(values._raw[key]); - mapLogger.debug({ msg: 'Extracted Attribute', key, type: typeof values[key], value: values[key] }); + const dataType = typeof values[key]; + // eslint-disable-next-line no-control-regex + if (dataType === 'string' && values[key].length > 100 && /[\x00-\x1F]/.test(values[key])) { + mapLogger.debug({ msg: 'Extracted Attribute', key, type: dataType, length: values[key].length, value: `${ values[key].substr(0, 100) }...` }); + return; + } + + mapLogger.debug({ msg: 'Extracted Attribute', key, type: dataType, value: values[key] }); }); return values;