diff --git a/.changeset/warm-hornets-ring.md b/.changeset/warm-hornets-ring.md new file mode 100644 index 00000000000..f81cf1efbe9 --- /dev/null +++ b/.changeset/warm-hornets-ring.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/core-typings": patch +--- + +Use group filter when set to LDAP sync process diff --git a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts index 854e941014a..f8a749455e0 100644 --- a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts +++ b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts @@ -153,7 +153,7 @@ export class ImportDataConverter { _id: new ObjectId().toHexString(), data, dataType: type, - ...options, + options, }); } @@ -454,13 +454,14 @@ export class ImportDataConverter { const batchToInsert = new Set(); - for await (const { data, _id } of users) { + for await (const record of users) { + const { data, _id } = record; if (this.aborted) { break; } try { - if (beforeImportFn && !(await beforeImportFn(data, 'user'))) { + if (beforeImportFn && !(await beforeImportFn(record))) { await this.skipRecord(_id); skippedCount++; continue; @@ -537,7 +538,7 @@ export class ImportDataConverter { } if (afterImportFn) { - await afterImportFn(data, 'user', isNewUser); + await afterImportFn(record, isNewUser); } } catch (e) { this._logger.error(e); @@ -741,13 +742,14 @@ export class ImportDataConverter { const rids: Array = []; const messages = await this.getMessagesToImport(); - for await (const { data, _id } of messages) { + for await (const record of messages) { + const { data, _id } = record; if (this.aborted) { return; } try { - if (beforeImportFn && !(await beforeImportFn(data, 'message'))) { + if (beforeImportFn && !(await beforeImportFn(record))) { await this.skipRecord(_id); continue; } @@ -816,7 +818,7 @@ export class ImportDataConverter { } if (afterImportFn) { - await afterImportFn(data, 'message', true); + await afterImportFn(record, true); } } catch (e) { await this.saveError(_id, e instanceof Error ? e : new Error(String(e))); @@ -1116,13 +1118,14 @@ export class ImportDataConverter { async convertChannels(startedByUserId: string, { beforeImportFn, afterImportFn, onErrorFn }: IConversionCallbacks = {}): Promise { const channels = await this.getChannelsToImport(); - for await (const { data, _id } of channels) { + for await (const record of channels) { + const { data, _id } = record; if (this.aborted) { return; } try { - if (beforeImportFn && !(await beforeImportFn(data, 'channel'))) { + if (beforeImportFn && !(await beforeImportFn(record))) { await this.skipRecord(_id); continue; } @@ -1151,7 +1154,7 @@ export class ImportDataConverter { } if (afterImportFn) { - await afterImportFn(data, 'channel', !existingRoom); + await afterImportFn(record, !existingRoom); } } catch (e) { await this.saveError(_id, e instanceof Error ? e : new Error(String(e))); diff --git a/apps/meteor/app/importer/server/classes/VirtualDataConverter.ts b/apps/meteor/app/importer/server/classes/VirtualDataConverter.ts index 595771d89d2..ef850226be5 100644 --- a/apps/meteor/app/importer/server/classes/VirtualDataConverter.ts +++ b/apps/meteor/app/importer/server/classes/VirtualDataConverter.ts @@ -56,8 +56,8 @@ export class VirtualDataConverter extends ImportDataConverter { return undefined; } - public addUserSync(data: IImportUser): void { - return this.addObjectSync('user', data); + public addUserSync(data: IImportUser, options?: Record): void { + return this.addObjectSync('user', data, options); } protected async addObject(type: IImportRecordType, data: IImportData, options: Record = {}): Promise { @@ -79,7 +79,7 @@ export class VirtualDataConverter extends ImportDataConverter { _id: Random.id(), data, dataType: type, - ...options, + options, }); } diff --git a/apps/meteor/app/importer/server/definitions/IConversionCallbacks.ts b/apps/meteor/app/importer/server/definitions/IConversionCallbacks.ts index 27d0c38b69d..262970dd60d 100644 --- a/apps/meteor/app/importer/server/definitions/IConversionCallbacks.ts +++ b/apps/meteor/app/importer/server/definitions/IConversionCallbacks.ts @@ -1,11 +1,11 @@ -import type { IImportUser, IImportMessage, IImportChannel, IImportRecordType } from '@rocket.chat/core-typings'; +import type { IImportRecord } from '@rocket.chat/core-typings'; -type ImporterBeforeImportCallback = { - (data: IImportUser | IImportChannel | IImportMessage, type: IImportRecordType): Promise; +export type ImporterBeforeImportCallback = { + (data: IImportRecord): Promise; }; export type ImporterAfterImportCallback = { - (data: IImportUser | IImportChannel | IImportMessage, type: IImportRecordType, isNewRecord: boolean): Promise; + (data: IImportRecord, isNewRecord: boolean): Promise; }; export interface IConversionCallbacks { diff --git a/apps/meteor/ee/server/lib/ldap/Manager.ts b/apps/meteor/ee/server/lib/ldap/Manager.ts index 5c40c282702..deb6cdcec66 100644 --- a/apps/meteor/ee/server/lib/ldap/Manager.ts +++ b/apps/meteor/ee/server/lib/ldap/Manager.ts @@ -1,9 +1,12 @@ import { Team } from '@rocket.chat/core-services'; -import type { ILDAPEntry, IUser, IRoom, IRole, IImportUser } from '@rocket.chat/core-typings'; +import type { ILDAPEntry, IUser, IRoom, IRole, IImportUser, IImportRecord } from '@rocket.chat/core-typings'; import { Users as UsersRaw, Roles, Subscriptions as SubscriptionsRaw, Rooms } from '@rocket.chat/models'; import type ldapjs from 'ldapjs'; -import type { ImporterAfterImportCallback } from '../../../../app/importer/server/definitions/IConversionCallbacks'; +import type { + ImporterAfterImportCallback, + ImporterBeforeImportCallback, +} from '../../../../app/importer/server/definitions/IConversionCallbacks'; import { addUserToRoom } from '../../../../app/lib/server/functions/addUserToRoom'; import { createRoom } from '../../../../app/lib/server/functions/createRoom'; import { removeUserFromRoom } from '../../../../app/lib/server/functions/removeUserFromRoom'; @@ -43,9 +46,22 @@ export class LDAPEEManager extends LDAPManager { await this.updateExistingUsers(ldap, converter); } + const membersOfGroupFilter = await ldap.searchMembersOfGroupFilter(); + await converter.convertUsers({ - afterImportFn: (async (data: IImportUser, _type: string, isNewRecord: boolean): Promise => - this.advancedSync(ldap, data, converter, isNewRecord)) as ImporterAfterImportCallback, + beforeImportFn: (async ({ options }: IImportRecord): Promise => { + if (!ldap.options.groupFilterEnabled || !ldap.options.groupFilterGroupMemberFormat) { + return true; + } + + const memberFormat = ldap.options.groupFilterGroupMemberFormat + ?.replace(/#{username}/g, options?.username || '#{username}') + .replace(/#{userdn}/g, options?.dn || '#{userdn}'); + + return membersOfGroupFilter.includes(memberFormat); + }) as ImporterBeforeImportCallback, + afterImportFn: (async ({ data }, isNewRecord: boolean): Promise => + this.advancedSync(ldap, data as IImportUser, converter, isNewRecord)) as ImporterAfterImportCallback, }); } catch (error) { logger.error(error); @@ -540,7 +556,7 @@ export class LDAPEEManager extends LDAPManager { count++; const userData = this.mapUserData(data); - converter.addUserSync(userData); + converter.addUserSync(userData, { dn: data.dn, username: this.getLdapUsername(data) }); return userData; }, endCallback: (error: any): void => { @@ -564,7 +580,7 @@ export class LDAPEEManager extends LDAPManager { if (ldapUser) { const userData = this.mapUserData(ldapUser, user.username); - converter.addUserSync(userData); + converter.addUserSync(userData, { dn: ldapUser.dn, username: this.getLdapUsername(ldapUser) }); } } } diff --git a/apps/meteor/server/lib/ldap/Connection.ts b/apps/meteor/server/lib/ldap/Connection.ts index f4163bccc97..2ab6ba9c73c 100644 --- a/apps/meteor/server/lib/ldap/Connection.ts +++ b/apps/meteor/server/lib/ldap/Connection.ts @@ -9,6 +9,7 @@ import type { import ldapjs from 'ldapjs'; import { settings } from '../../../app/settings/server'; +import { ensureArray } from '../../../lib/utils/arrayUtils'; import { logger, connLogger, searchLogger, authLogger, bindLogger, mapLogger } from './Logger'; import { getLDAPConditionalSetting } from './getLDAPConditionalSetting'; @@ -391,6 +392,49 @@ export class LDAPConnection { return `(&${filter.join('')})`; } + public async searchMembersOfGroupFilter(): Promise { + if (!this.options.groupFilterEnabled) { + return []; + } + + if (!this.options.groupFilterGroupMemberAttribute) { + return []; + } + + if (!this.options.groupFilterGroupMemberFormat) { + searchLogger.debug(`LDAP Group Filter is enabled but no group member format is set.`); + return []; + } + + const filter = ['(&']; + + if (this.options.groupFilterObjectClass) { + filter.push(`(objectclass=${this.options.groupFilterObjectClass})`); + } + + if (this.options.groupFilterGroupIdAttribute) { + filter.push(`(${this.options.groupFilterGroupIdAttribute}=${this.options.groupFilterGroupName})`); + } + filter.push(')'); + const searchOptions: ldapjs.SearchOptions = { + filter: filter.join(''), + scope: 'sub', + }; + + searchLogger.debug({ msg: 'Group filter LDAP:', filter: searchOptions.filter }); + + const result = await this.searchRaw(this.options.baseDN, searchOptions); + + if (!Array.isArray(result) || result.length === 0) { + searchLogger.debug({ msg: 'No groups found', result }); + return []; + } + + const members = this.extractLdapAttribute(result[0].raw[this.options.groupFilterGroupMemberAttribute]) as string | string[]; + + return ensureArray(members); + } + public async isUserAcceptedByGroupFilter(username: string, userdn: string): Promise { if (!this.options.groupFilterEnabled) { return true; diff --git a/apps/meteor/server/lib/ldap/Manager.ts b/apps/meteor/server/lib/ldap/Manager.ts index e3529709f08..99fe356d53c 100644 --- a/apps/meteor/server/lib/ldap/Manager.ts +++ b/apps/meteor/server/lib/ldap/Manager.ts @@ -483,7 +483,7 @@ export class LDAPManager { return this.slugify(requestUsername); } - private static getLdapUsername(ldapUser: ILDAPEntry): string | undefined { + protected static getLdapUsername(ldapUser: ILDAPEntry): string | undefined { const usernameField = getLDAPConditionalSetting('LDAP_Username_Field') as string; return this.getLdapDynamicValue(ldapUser, usernameField); } diff --git a/packages/core-typings/src/import/IImportRecord.ts b/packages/core-typings/src/import/IImportRecord.ts index 4fbeb37c210..9adf58e284e 100644 --- a/packages/core-typings/src/import/IImportRecord.ts +++ b/packages/core-typings/src/import/IImportRecord.ts @@ -9,7 +9,7 @@ export interface IImportRecord { data: IImportData; dataType: IImportRecordType; _id: string; - options?: Record; + options?: Record; errors?: Array<{ message: string; stack?: string;