From e1e603e8221f43e62f4d46cf89717fa8e89c16e6 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Sat, 18 Sep 2021 21:37:52 -0300 Subject: [PATCH 01/13] Restored Authentication --- definition/ldap/ILDAPOptions.ts | 4 ++ ee/definition/ldap/ILDAPEEOptions.ts | 5 --- ee/server/configuration/ldap.ts | 6 --- ee/server/lib/ldap/Connection.ts | 65 ---------------------------- ee/server/lib/ldap/Manager.ts | 5 +-- server/lib/ldap/Connection.ts | 35 ++++++++++++++- server/lib/ldap/Manager.ts | 16 ++----- server/settings/ldap.ts | 22 +++++----- 8 files changed, 53 insertions(+), 105 deletions(-) delete mode 100644 ee/definition/ldap/ILDAPEEOptions.ts delete mode 100644 ee/server/lib/ldap/Connection.ts diff --git a/definition/ldap/ILDAPOptions.ts b/definition/ldap/ILDAPOptions.ts index 32a8494de1d5..e7721dcd041d 100644 --- a/definition/ldap/ILDAPOptions.ts +++ b/definition/ldap/ILDAPOptions.ts @@ -24,4 +24,8 @@ export interface ILDAPConnectionOptions { groupFilterGroupMemberAttribute?: string; groupFilterGroupMemberFormat?: string; groupFilterGroupName?: string; + authentication: boolean; + authenticationUserDN: string; + authenticationPassword: string; + attributesToQuery: Array; } diff --git a/ee/definition/ldap/ILDAPEEOptions.ts b/ee/definition/ldap/ILDAPEEOptions.ts deleted file mode 100644 index 35d62f46fa1f..000000000000 --- a/ee/definition/ldap/ILDAPEEOptions.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface ILDAPEEConnectionOptions { - authentication: boolean; - authenticationUserDN: string; - authenticationPassword: string; -} diff --git a/ee/server/configuration/ldap.ts b/ee/server/configuration/ldap.ts index 1b89f5338e3d..947a4a601e94 100644 --- a/ee/server/configuration/ldap.ts +++ b/ee/server/configuration/ldap.ts @@ -6,10 +6,8 @@ import { LDAPEE } from '../sdk'; import { settings } from '../../../app/settings/server'; import { logger } from '../../../server/lib/ldap/Logger'; import { cronJobs } from '../../../app/utils/server/lib/cron/Cronjobs'; -import { LDAPEEConnection } from '../lib/ldap/Connection'; import { LDAPEEManager } from '../lib/ldap/Manager'; import { callbacks } from '../../../app/callbacks/server'; -import type { LDAPConnection } from '../../../server/lib/ldap/Connection'; import type { IImportUser } from '../../../definition/IImportUser'; import type { ILDAPEntry } from '../../../definition/ldap/ILDAPEntry'; import { onLicense } from '../../app/license/server'; @@ -46,10 +44,6 @@ onLicense('ldap-enterprise', () => { }); }); - callbacks.add('getLDAPConnectionClass', function(): typeof LDAPConnection { - return LDAPEEConnection; - }, callbacks.priority.HIGH, 'replaceLDAPConnectionClass'); - callbacks.add('mapLDAPUserData', (userData: IImportUser, ldapUser: ILDAPEntry) => { LDAPEEManager.copyCustomFields(ldapUser, userData); LDAPEEManager.copyActiveState(ldapUser, userData); diff --git a/ee/server/lib/ldap/Connection.ts b/ee/server/lib/ldap/Connection.ts deleted file mode 100644 index 13daa8d290b4..000000000000 --- a/ee/server/lib/ldap/Connection.ts +++ /dev/null @@ -1,65 +0,0 @@ -import ldapjs from 'ldapjs'; - -import { LDAPConnection } from '../../../../server/lib/ldap/Connection'; -import { logger, bindLogger } from '../../../../server/lib/ldap/Logger'; -import { settings } from '../../../../app/settings/server'; -import type { ILDAPEEConnectionOptions } from '../../../definition/ldap/ILDAPEEOptions'; - -export class LDAPEEConnection extends LDAPConnection { - public eeOptions: ILDAPEEConnectionOptions; - - private usingAuthentication: boolean; - - constructor() { - super(); - - this.eeOptions = { - authentication: settings.get('LDAP_Authentication') ?? false, - authenticationUserDN: settings.get('LDAP_Authentication_UserDN') ?? '', - authenticationPassword: settings.get('LDAP_Authentication_Password') ?? '', - }; - } - - /* - Bind UserDN and Password if specified and not yet bound - */ - public async maybeBindDN(): Promise { - if (this.usingAuthentication) { - return; - } - - if (!this.eeOptions.authentication) { - return; - } - - if (!this.eeOptions.authenticationUserDN) { - logger.error('Invalid UserDN for authentication'); - return; - } - - bindLogger.info({ msg: 'Binding UserDN', userDN: this.eeOptions.authenticationUserDN }); - await this.bindDN(this.eeOptions.authenticationUserDN, this.eeOptions.authenticationPassword); - this.usingAuthentication = true; - } - - public disconnect(): void { - this.usingAuthentication = false; - super.disconnect(); - } - - public async testConnection(): Promise { - await super.testConnection(); - - await this.maybeBindDN(); - } - - protected async runBeforeSearch(searchOptions: ldapjs.SearchOptions): Promise { - await this.maybeBindDN(); - - if (!Array.isArray(searchOptions.attributes)) { - searchOptions.attributes = searchOptions.attributes ? [searchOptions.attributes] : ['*']; - } - searchOptions.attributes.push('pwdAccountLockedTime'); - super.runBeforeSearch(searchOptions); - } -} diff --git a/ee/server/lib/ldap/Manager.ts b/ee/server/lib/ldap/Manager.ts index 2c60f326f799..8b07b1848f39 100644 --- a/ee/server/lib/ldap/Manager.ts +++ b/ee/server/lib/ldap/Manager.ts @@ -15,11 +15,10 @@ import { Subscriptions as SubscriptionsRaw, } from '../../../../app/models/server/raw'; import { LDAPDataConverter } from '../../../../server/lib/ldap/DataConverter'; -import type { LDAPConnection } from '../../../../server/lib/ldap/Connection'; +import { LDAPConnection } from '../../../../server/lib/ldap/Connection'; import { LDAPManager } from '../../../../server/lib/ldap/Manager'; import { logger } from '../../../../server/lib/ldap/Logger'; import { templateVarHandler } from '../../../../app/utils/lib/templateVarHandler'; -import { LDAPEEConnection } from './Connection'; import { api } from '../../../../server/sdk/api'; import { addUserToRoom, removeUserFromRoom, createRoom } from '../../../../app/lib/server/functions'; import { Team } from '../../../../server/sdk'; @@ -31,7 +30,7 @@ export class LDAPEEManager extends LDAPManager { } const options = this.getConverterOptions(); - const ldap = new LDAPEEConnection(); + const ldap = new LDAPConnection(); const converter = new LDAPDataConverter(true, options); try { diff --git a/server/lib/ldap/Connection.ts b/server/lib/ldap/Connection.ts index d118508147b3..f8f5f31033ef 100644 --- a/server/lib/ldap/Connection.ts +++ b/server/lib/ldap/Connection.ts @@ -5,7 +5,7 @@ import type { ILDAPConnectionOptions, LDAPEncryptionType, LDAPSearchScope } from import type { ILDAPEntry } from '../../../definition/ldap/ILDAPEntry'; import type { ILDAPCallback, ILDAPPageCallback } from '../../../definition/ldap/ILDAPCallback'; import { callbacks } from '../../../app/callbacks/server'; -import { logger, connLogger, searchLogger, authLogger } from './Logger'; +import { logger, connLogger, searchLogger, authLogger, bindLogger } from './Logger'; import { getLDAPConditionalSetting } from './getLDAPConditionalSetting'; interface ILDAPEntryCallback { @@ -43,6 +43,8 @@ export class LDAPConnection { private _connectionCallback: ILDAPCallback; + private usingAuthentication: boolean; + constructor() { this.ldapjs = ldapjs; @@ -73,6 +75,10 @@ export class LDAPConnection { groupFilterGroupMemberAttribute: settings.get('LDAP_Group_Filter_Group_Member_Attribute'), groupFilterGroupMemberFormat: settings.get('LDAP_Group_Filter_Group_Member_Format'), groupFilterGroupName: settings.get('LDAP_Group_Filter_Group_Name'), + authentication: settings.get('LDAP_Authentication') ?? false, + authenticationUserDN: settings.get('LDAP_Authentication_UserDN') ?? '', + authenticationPassword: settings.get('LDAP_Authentication_Password') ?? '', + attributesToQuery: this.parseAttributeList(settings.get('LDAP_User_Search_AttributesToQuery')), }; if (!this.options.host) { @@ -96,6 +102,7 @@ export class LDAPConnection { } public disconnect(): void { + this.usingAuthentication = false; this.connected = false; connLogger.info('Disconnecting'); @@ -105,7 +112,12 @@ export class LDAPConnection { } public async testConnection(): Promise { - await this.connect(); + try { + await this.connect(); + await this.maybeBindDN(); + } finally { + this.disconnect(); + } } public async searchByUsername(escapedUsername: string): Promise { @@ -515,6 +527,25 @@ export class LDAPConnection { this.client._updateIdle(override); } + protected async maybeBindDN(): Promise { + if (this.usingAuthentication) { + return; + } + + if (!this.options.authentication) { + return; + } + + if (!this.options.authenticationUserDN) { + logger.error('Invalid UserDN for authentication'); + return; + } + + bindLogger.info({ msg: 'Binding UserDN', userDN: this.options.authenticationUserDN }); + await this.bindDN(this.options.authenticationUserDN, this.options.authenticationPassword); + this.usingAuthentication = true; + } + protected async runBeforeSearch(searchOptions: ldapjs.SearchOptions): Promise { callbacks.run('beforeLDAPSearch', searchOptions, this); } diff --git a/server/lib/ldap/Manager.ts b/server/lib/ldap/Manager.ts index a8bfabadaa8a..388027f4c0d6 100644 --- a/server/lib/ldap/Manager.ts +++ b/server/lib/ldap/Manager.ts @@ -31,7 +31,7 @@ export class LDAPManager { let ldapUser: ILDAPEntry | undefined; - const ldap = this.getNewConnection(); + const ldap = new LDAPConnection(); try { try { await ldap.connect(); @@ -57,20 +57,10 @@ export class LDAPManager { } } - public static getNewConnection(): LDAPConnection { - const ClassRef = callbacks.run('getLDAPConnectionClass') || LDAPConnection; - - return new ClassRef(); - } - public static async testConnection(): Promise { try { - const ldap = LDAPManager.getNewConnection(); - try { - await ldap.testConnection(); - } finally { - ldap.disconnect(); - } + const ldap = new LDAPConnection(); + await ldap.testConnection(); } catch (error) { connLogger.error(error); throw error; diff --git a/server/settings/ldap.ts b/server/settings/ldap.ts index 6582db256a2d..63af147b9f9c 100644 --- a/server/settings/ldap.ts +++ b/server/settings/ldap.ts @@ -24,6 +24,17 @@ settings.addGroup('LDAP', function() { this.add('LDAP_Login_Fallback', false, { type: 'boolean', enableQuery }); + this.section('LDAP_Connection_Authentication', function() { + const enableAuthentication = [ + enableQuery, + { _id: 'LDAP_Authentication', value: true }, + ]; + + this.add('LDAP_Authentication', false, { type: 'boolean', enableQuery, invalidValue: false }); + this.add('LDAP_Authentication_UserDN', '', { type: 'string', enableQuery: enableAuthentication, secret: true, invalidValue: '' }); + this.add('LDAP_Authentication_Password', '', { type: 'password', enableQuery: enableAuthentication, secret: true, invalidValue: '' }); + }); + this.section('LDAP_Connection_Encryption', function() { this.add('LDAP_Encryption', 'plain', { type: 'select', @@ -191,17 +202,6 @@ settings.addGroup('LDAP', function() { enterprise: true, modules: ['ldap-enterprise'], }, function() { - this.section('LDAP_Connection_Authentication', function() { - const enableAuthentication = [ - enableQuery, - { _id: 'LDAP_Authentication', value: true }, - ]; - - this.add('LDAP_Authentication', false, { type: 'boolean', enableQuery, invalidValue: false }); - this.add('LDAP_Authentication_UserDN', '', { type: 'string', enableQuery: enableAuthentication, secret: true, invalidValue: '' }); - this.add('LDAP_Authentication_Password', '', { type: 'password', enableQuery: enableAuthentication, secret: true, invalidValue: '' }); - }); - this.section('LDAP_DataSync_BackgroundSync', function() { this.add('LDAP_Background_Sync', false, { type: 'boolean', From f8b1499a07a158c9389b48b992903114b3267920 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Sat, 18 Sep 2021 21:38:33 -0300 Subject: [PATCH 02/13] [FIX] background sync schedule not updating --- ee/server/configuration/ldap.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ee/server/configuration/ldap.ts b/ee/server/configuration/ldap.ts index 947a4a601e94..d4709f324d35 100644 --- a/ee/server/configuration/ldap.ts +++ b/ee/server/configuration/ldap.ts @@ -15,6 +15,7 @@ import { onLicense } from '../../app/license/server'; onLicense('ldap-enterprise', () => { // Configure background sync cronjob const jobName = 'LDAP_Sync'; + let lastSchedule: string; const addCronJob = _.debounce(Meteor.bindEnvironment(function addCronJobDebounced() { if (settings.get('LDAP_Background_Sync') !== true) { logger.info('Disabling LDAP Background Sync'); @@ -26,6 +27,11 @@ onLicense('ldap-enterprise', () => { const schedule = settings.get('LDAP_Background_Sync_Interval'); if (schedule) { + if (schedule !== lastSchedule && cronJobs.nextScheduledAtDate(jobName)) { + cronJobs.remove(jobName); + } + + lastSchedule = schedule; logger.info('Enabling LDAP Background Sync'); cronJobs.add(jobName, schedule, () => Promise.await(LDAPEE.sync()), 'text'); } From a8e97317ebe3804d2ead66f17d24880aa81a4514 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Sat, 18 Sep 2021 21:39:46 -0300 Subject: [PATCH 03/13] New setting to specify attributes to query for --- packages/rocketchat-i18n/i18n/en.i18n.json | 4 +++- server/lib/ldap/Connection.ts | 21 +++++++++++++++++++-- server/lib/ldap/Logger.ts | 1 + server/lib/ldap/Manager.ts | 2 +- server/settings/ldap.ts | 9 +++++++-- 5 files changed, 31 insertions(+), 6 deletions(-) diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index d96ad2739938..620f17fcdc36 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -2425,7 +2425,7 @@ "LDAP_DataSync": "Data Sync", "LDAP_DataSync_DataMap": "Mapping", "LDAP_DataSync_Avatar": "Avatar", - "LDAP_DataSync_ActiveState": "User Active State", + "LDAP_DataSync_Advanced": "Advanced Sync", "LDAP_DataSync_CustomFields": "Sync Custom Fields", "LDAP_DataSync_Roles": "Sync Roles", "LDAP_DataSync_Channels": "Sync Channels", @@ -2535,6 +2535,8 @@ "LDAP_Timeout_Description": "How many mileseconds wait for a search result before return an error", "LDAP_Unique_Identifier_Field": "Unique Identifier Field", "LDAP_Unique_Identifier_Field_Description": "Which field will be used to link the LDAP user and the Rocket.Chat user. You can inform multiple values separated by comma to try to get the value from LDAP record.
Default value is `objectGUID,ibm-entryUUID,GUID,dominoUNID,nsuniqueId,uidNumber`", + "LDAP_User_Search_AttributesToQuery": "Attributes to Query", + "LDAP_User_Search_AttributesToQuery_Description": "Specify which attributes should be returned on LDAP queries, separating them with commas. Defaults to everything. `*` represents all regular attributes and `+` represents all operational attributes.", "LDAP_User_Search_Field": "Search Field", "LDAP_User_Search_Field_Description": "The LDAP attribute that identifies the LDAP user who attempts authentication. This field should be `sAMAccountName` for most Active Directory installations, but it may be `uid` for other LDAP solutions, such as OpenLDAP. You can use `mail` to identify users by email or whatever attribute you want.
You can use multiple values separated by comma to allow users to login using multiple identifiers like username or email.", "LDAP_User_Search_Filter": "Filter", diff --git a/server/lib/ldap/Connection.ts b/server/lib/ldap/Connection.ts index f8f5f31033ef..532022081d4a 100644 --- a/server/lib/ldap/Connection.ts +++ b/server/lib/ldap/Connection.ts @@ -5,7 +5,7 @@ import type { ILDAPConnectionOptions, LDAPEncryptionType, LDAPSearchScope } from import type { ILDAPEntry } from '../../../definition/ldap/ILDAPEntry'; import type { ILDAPCallback, ILDAPPageCallback } from '../../../definition/ldap/ILDAPCallback'; import { callbacks } from '../../../app/callbacks/server'; -import { logger, connLogger, searchLogger, authLogger, bindLogger } from './Logger'; +import { logger, connLogger, searchLogger, authLogger, bindLogger, mapLogger } from './Logger'; import { getLDAPConditionalSetting } from './getLDAPConditionalSetting'; interface ILDAPEntryCallback { @@ -125,6 +125,7 @@ export class LDAPConnection { filter: this.getUserFilter(escapedUsername), scope: this.options.userSearchScope || 'sub', sizeLimit: this.options.searchSizeLimit, + attributes: this.options.attributesToQuery, }; if (this.options.searchPageSize > 0) { @@ -149,7 +150,7 @@ export class LDAPConnection { public async searchById(id: string, attribute?: string): Promise { const searchOptions: ldapjs.SearchOptions = { scope: this.options.userSearchScope || 'sub', - attributes: ['*', '+'], + attributes: this.options.attributesToQuery, }; if (attribute) { @@ -196,6 +197,7 @@ export class LDAPConnection { filter: this.getUserFilter('*'), scope: this.options.userSearchScope || 'sub', sizeLimit: this.options.searchSizeLimit, + attributes: this.options.attributesToQuery, }; if (this.options.searchPageSize > 0) { @@ -276,6 +278,8 @@ 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] }); }); return values; @@ -676,4 +680,17 @@ export class LDAPConnection { } }, clientOptions.connectTimeout); } + + private parseAttributeList(csv: string | undefined): Array { + if (!csv) { + return ['*', '+']; + } + + const list = csv.split(',').map((item) => item.trim()); + if (!list?.length) { + return ['*', '+']; + } + + return list; + } } diff --git a/server/lib/ldap/Logger.ts b/server/lib/ldap/Logger.ts index 14bdc80af4e2..1b2341dc548a 100644 --- a/server/lib/ldap/Logger.ts +++ b/server/lib/ldap/Logger.ts @@ -5,3 +5,4 @@ export const connLogger = logger.section('Connection'); export const bindLogger = logger.section('Bind'); export const searchLogger = logger.section('Search'); export const authLogger = logger.section('Auth'); +export const mapLogger = logger.section('Mapping'); diff --git a/server/lib/ldap/Manager.ts b/server/lib/ldap/Manager.ts index 388027f4c0d6..37f454823906 100644 --- a/server/lib/ldap/Manager.ts +++ b/server/lib/ldap/Manager.ts @@ -205,7 +205,7 @@ export class LDAPManager { } private static async syncUserForLogin(ldapUser: ILDAPEntry, existingUser?: IUser, usedUsername?: string | undefined): Promise { - logger.debug({ msg: 'Syncing user data', ldapUser, user: { ...existingUser && { email: existingUser.emails, _id: existingUser._id } } }); + logger.debug({ msg: 'Syncing user data', ldapUser: _.omit(ldapUser, '_raw'), user: { ...existingUser && { email: existingUser.emails, _id: existingUser._id } } }); const userData = this.mapUserData(ldapUser, usedUsername); const options = this.getConverterOptions(); diff --git a/server/settings/ldap.ts b/server/settings/ldap.ts index 63af147b9f9c..def1bfeda552 100644 --- a/server/settings/ldap.ts +++ b/server/settings/ldap.ts @@ -233,7 +233,7 @@ settings.addGroup('LDAP', function() { }); }); - this.section('LDAP_DataSync_ActiveState', function() { + this.section('LDAP_DataSync_Advanced', function() { this.add('LDAP_Sync_User_Active_State', 'disable', { type: 'select', values: [ @@ -243,9 +243,14 @@ settings.addGroup('LDAP', function() { ], i18nDescription: 'LDAP_Sync_User_Active_State_Description', enableQuery: { _id: 'LDAP_Enable', value: true }, - enterprise: true, invalidValue: 'none', }); + + this.add('LDAP_User_Search_AttributesToQuery', '*,+', { + type: 'string', + enableQuery: { _id: 'LDAP_Enable', value: true }, + invalidValue: '*,+', + }); }); this.section('LDAP_DataSync_CustomFields', function() { From e90342482f61323b9db1398f00c7bee2d108c899 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Sat, 18 Sep 2021 21:40:07 -0300 Subject: [PATCH 04/13] Changed update data on login default to true --- server/lib/ldap/Manager.ts | 2 +- server/settings/ldap.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/lib/ldap/Manager.ts b/server/lib/ldap/Manager.ts index 37f454823906..5c036975e4d4 100644 --- a/server/lib/ldap/Manager.ts +++ b/server/lib/ldap/Manager.ts @@ -194,7 +194,7 @@ export class LDAPManager { throw new Meteor.Error('LDAP-login-error', `LDAP Authentication succeeded, but there's already an existing user with provided username [${ user.username }] in Mongo.`); } - const syncData = settings.get('LDAP_Update_Data_On_Login') ?? false; + const syncData = settings.get('LDAP_Update_Data_On_Login') ?? true; logger.debug({ msg: 'Logging user in', syncData }); const updatedUser = (syncData && await this.syncUserForLogin(ldapUser, user)) || user; diff --git a/server/settings/ldap.ts b/server/settings/ldap.ts index def1bfeda552..ffb9dd75d8bd 100644 --- a/server/settings/ldap.ts +++ b/server/settings/ldap.ts @@ -130,7 +130,7 @@ settings.addGroup('LDAP', function() { enableQuery, }); - this.add('LDAP_Update_Data_On_Login', false, { + this.add('LDAP_Update_Data_On_Login', true, { type: 'boolean', enableQuery, }); From cf8fa7d8be278ff29800aca1b48f92cf56735a87 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Sat, 18 Sep 2021 21:40:37 -0300 Subject: [PATCH 05/13] Changed group filter group member format setting default --- server/settings/ldap.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/settings/ldap.ts b/server/settings/ldap.ts index ffb9dd75d8bd..3d772eaabd41 100644 --- a/server/settings/ldap.ts +++ b/server/settings/ldap.ts @@ -114,7 +114,7 @@ settings.addGroup('LDAP', function() { this.add('LDAP_Group_Filter_ObjectClass', 'groupOfUniqueNames', { type: 'string', enableQuery: groupFilterQuery }); this.add('LDAP_Group_Filter_Group_Id_Attribute', 'cn', { type: 'string', enableQuery: groupFilterQuery }); this.add('LDAP_Group_Filter_Group_Member_Attribute', 'uniqueMember', { type: 'string', enableQuery: groupFilterQuery }); - this.add('LDAP_Group_Filter_Group_Member_Format', 'uniqueMember', { type: 'string', enableQuery: groupFilterQuery }); + this.add('LDAP_Group_Filter_Group_Member_Format', '', { type: 'string', enableQuery: groupFilterQuery }); this.add('LDAP_Group_Filter_Group_Name', 'ROCKET_CHAT', { type: 'string', enableQuery: groupFilterQuery }); }); }); From 982145ee16feefd0afdebf15455c09086b836604 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Sat, 18 Sep 2021 23:47:31 -0300 Subject: [PATCH 06/13] Added avatar background sync and a check to prevent the same avatar from being uploaded to the file store repeatedly. --- app/lib/server/functions/setUserAvatar.js | 7 ++- app/models/server/models/Users.js | 3 +- definition/IUser.ts | 1 + ee/server/configuration/ldap.ts | 44 ++++++++++------- ee/server/lib/ldap/Manager.ts | 56 +++++++++++++++++++++- ee/server/local-services/ldap/service.ts | 4 ++ ee/server/sdk/types/ILDAPEEService.ts | 1 + packages/rocketchat-i18n/i18n/en.i18n.json | 5 +- server/lib/ldap/Manager.ts | 37 +++++++------- server/settings/ldap.ts | 15 ++++++ 10 files changed, 133 insertions(+), 40 deletions(-) diff --git a/app/lib/server/functions/setUserAvatar.js b/app/lib/server/functions/setUserAvatar.js index 5bcd36224583..1a39045eb51d 100644 --- a/app/lib/server/functions/setUserAvatar.js +++ b/app/lib/server/functions/setUserAvatar.js @@ -1,5 +1,6 @@ import { Meteor } from 'meteor/meteor'; import { HTTP } from 'meteor/http'; +import { SHA256 } from 'meteor/sha'; import { RocketChatFile } from '../../../file/server'; import { FileUpload } from '../../../file-upload/server'; @@ -12,7 +13,7 @@ export const setUserAvatar = function(user, dataURI, contentType, service) { let image; if (service === 'initials') { - return Users.setAvatarData(user._id, service, null); + return Users.setAvatarData(user._id, service, null, null); } if (service === 'url') { let result = null; @@ -53,6 +54,8 @@ export const setUserAvatar = function(user, dataURI, contentType, service) { } const buffer = Buffer.from(image, encoding); + const hash = SHA256(buffer.toString()); + const fileStore = FileUpload.getStore('Avatars'); fileStore.deleteByName(user.username); @@ -64,7 +67,7 @@ export const setUserAvatar = function(user, dataURI, contentType, service) { fileStore.insert(file, buffer, (err, result) => { Meteor.setTimeout(function() { - Users.setAvatarData(user._id, service, result.etag); + Users.setAvatarData(user._id, service, result.etag, hash); api.broadcast('user.avatarUpdate', { username: user.username, avatarETag: result.etag }); }, 500); }); diff --git a/app/models/server/models/Users.js b/app/models/server/models/Users.js index 149978b18a6b..84137eb546a0 100644 --- a/app/models/server/models/Users.js +++ b/app/models/server/models/Users.js @@ -1138,11 +1138,12 @@ export class Users extends Base { return this.update(_id, update); } - setAvatarData(_id, origin, etag) { + setAvatarData(_id, origin, etag, hash = null) { const update = { $set: { avatarOrigin: origin, avatarETag: etag, + avatarHash: hash, }, }; diff --git a/definition/IUser.ts b/definition/IUser.ts index 732c684961ba..c3047482d8bf 100644 --- a/definition/IUser.ts +++ b/definition/IUser.ts @@ -121,6 +121,7 @@ export interface IUser extends IRocketChatRecord { lastLogin?: Date; avatarOrigin?: string; avatarETag?: string; + avatarHash?: string; utcOffset?: number; language?: string; statusDefault?: UserStatus; diff --git a/ee/server/configuration/ldap.ts b/ee/server/configuration/ldap.ts index d4709f324d35..60b72eb7bc0f 100644 --- a/ee/server/configuration/ldap.ts +++ b/ee/server/configuration/ldap.ts @@ -10,36 +10,44 @@ import { LDAPEEManager } from '../lib/ldap/Manager'; import { callbacks } from '../../../app/callbacks/server'; import type { IImportUser } from '../../../definition/IImportUser'; import type { ILDAPEntry } from '../../../definition/ldap/ILDAPEntry'; +import type { SettingCallback } from '../../../app/settings/lib/settings'; import { onLicense } from '../../app/license/server'; onLicense('ldap-enterprise', () => { // Configure background sync cronjob - const jobName = 'LDAP_Sync'; - let lastSchedule: string; - const addCronJob = _.debounce(Meteor.bindEnvironment(function addCronJobDebounced() { - if (settings.get('LDAP_Background_Sync') !== true) { - logger.info('Disabling LDAP Background Sync'); - if (cronJobs.nextScheduledAtDate(jobName)) { - cronJobs.remove(jobName); + function configureBackgroundSync(jobName: string, enableSetting: string, intervalSetting: string, cb: () => {}): SettingCallback { + let lastSchedule: string; + + return _.debounce(Meteor.bindEnvironment(function addCronJobDebounced() { + if (settings.get(enableSetting) !== true) { + logger.info({ msg: 'Disabling LDAP Background Sync', jobName }); + if (cronJobs.nextScheduledAtDate(jobName)) { + cronJobs.remove(jobName); + } + return; } - return; - } - const schedule = settings.get('LDAP_Background_Sync_Interval'); - if (schedule) { - if (schedule !== lastSchedule && cronJobs.nextScheduledAtDate(jobName)) { - cronJobs.remove(jobName); + const schedule = settings.get(intervalSetting); + if (schedule) { + if (schedule !== lastSchedule && cronJobs.nextScheduledAtDate(jobName)) { + cronJobs.remove(jobName); + } + + lastSchedule = schedule; + logger.info({ msg: 'Enabling LDAP Background Sync', jobName }); + cronJobs.add(jobName, schedule, () => cb(), 'text'); } + }), 500); + } - lastSchedule = schedule; - logger.info('Enabling LDAP Background Sync'); - cronJobs.add(jobName, schedule, () => Promise.await(LDAPEE.sync()), 'text'); - } - }), 500); + const addCronJob = configureBackgroundSync('LDAP_Sync', 'LDAP_Background_Sync', 'LDAP_Background_Sync_Interval', () => Promise.await(LDAPEE.sync())); + const addAvatarCronJob = configureBackgroundSync('LDAP_AvatarSync', 'LDAP_Background_Sync_Avatars', 'LDAP_Background_Sync_Avatars_Interval', () => Promise.await(LDAPEE.syncAvatars())); Meteor.defer(() => { settings.get('LDAP_Background_Sync', addCronJob); settings.get('LDAP_Background_Sync_Interval', addCronJob); + settings.get('LDAP_Background_Sync_Avatars', addAvatarCronJob); + settings.get('LDAP_Background_Sync_Avatars_Interval', addAvatarCronJob); settings.get('LDAP_Groups_To_Rocket_Chat_Teams', (_key, value) => { try { diff --git a/ee/server/lib/ldap/Manager.ts b/ee/server/lib/ldap/Manager.ts index 8b07b1848f39..d7669c63200d 100644 --- a/ee/server/lib/ldap/Manager.ts +++ b/ee/server/lib/ldap/Manager.ts @@ -17,7 +17,7 @@ import { import { LDAPDataConverter } from '../../../../server/lib/ldap/DataConverter'; import { LDAPConnection } from '../../../../server/lib/ldap/Connection'; import { LDAPManager } from '../../../../server/lib/ldap/Manager'; -import { logger } from '../../../../server/lib/ldap/Logger'; +import { logger, searchLogger } from '../../../../server/lib/ldap/Logger'; import { templateVarHandler } from '../../../../app/utils/lib/templateVarHandler'; import { api } from '../../../../server/sdk/api'; import { addUserToRoom, removeUserFromRoom, createRoom } from '../../../../app/lib/server/functions'; @@ -25,7 +25,7 @@ import { Team } from '../../../../server/sdk'; export class LDAPEEManager extends LDAPManager { public static async sync(): Promise { - if (settings.get('LDAP_Enable') !== true) { + if (settings.get('LDAP_Enable') !== true || settings.get('LDAP_Background_Sync') !== true) { return; } @@ -57,6 +57,25 @@ export class LDAPEEManager extends LDAPManager { } } + public static async syncAvatars(): Promise { + if (settings.get('LDAP_Enable') !== true || settings.get('LDAP_Background_Sync_Avatars') !== true) { + return; + } + + try { + const ldap = new LDAPConnection(); + await ldap.connect(); + + try { + await this.updateUserAvatars(ldap); + } finally { + ldap.disconnect(); + } + } catch (error) { + logger.error(error); + } + } + public static validateLDAPTeamsMappingChanges(json: string): void { if (!json) { return; @@ -490,6 +509,39 @@ export class LDAPEEManager extends LDAPManager { }); } + private static async updateUserAvatars(ldap: LDAPConnection): Promise { + return new Promise(async (resolve, reject) => { + try { + const users = await UsersRaw.findLDAPUsers(); + for await (const user of users) { + let ldapUser: ILDAPEntry | undefined; + + if (user.services?.ldap?.id) { + ldapUser = await ldap.findOneById(user.services.ldap.id, user.services.ldap.idAttribute); + } else if (user.username) { + ldapUser = await ldap.findOneByUsername(user.username); + } + + if (!ldapUser) { + searchLogger.debug({ + msg: 'existing LDAP user not found during Avatar Sync', + ldapId: user.services?.ldap?.id, + ldapAttribute: user.services?.ldap?.idAttribute, + username: user.username, + }); + continue; + } + + LDAPManager.syncUserAvatar(user, ldapUser); + } + + resolve(); + } catch (error) { + reject(error); + } + }); + } + private static getCustomField(customFields: Record, property: string): any { try { return _.reduce(property.split('.'), (acc, el) => acc[el], customFields); diff --git a/ee/server/local-services/ldap/service.ts b/ee/server/local-services/ldap/service.ts index a09b22f46fba..14227ae9a7a0 100644 --- a/ee/server/local-services/ldap/service.ts +++ b/ee/server/local-services/ldap/service.ts @@ -11,6 +11,10 @@ export class LDAPEEService extends ServiceClass implements ILDAPEEService { async sync(): Promise { return LDAPEEManager.sync(); } + + async syncAvatars(): Promise { + return LDAPEEManager.syncAvatars(); + } } api.registerService(new LDAPEEService()); diff --git a/ee/server/sdk/types/ILDAPEEService.ts b/ee/server/sdk/types/ILDAPEEService.ts index 1d3d5bbd04f1..cadc6eedc3a7 100644 --- a/ee/server/sdk/types/ILDAPEEService.ts +++ b/ee/server/sdk/types/ILDAPEEService.ts @@ -1,3 +1,4 @@ export interface ILDAPEEService { sync(): Promise; + syncAvatars(): Promise; } diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 620f17fcdc36..e387ef040309 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -2446,6 +2446,9 @@ "LDAP_Avatar_Field": "User Avatar Field", "LDAP_Avatar_Field_Description": " Which field will be used as *avatar* for users. Leave empty to use `thumbnailPhoto` first and `jpegPhoto` as fallback.", "LDAP_Background_Sync": "Background Sync", + "LDAP_Background_Sync_Avatars": "Avatar Background Sync", + "LDAP_Background_Sync_Avatars_Description": "Enable a separate background process to sync user avatars.", + "LDAP_Background_Sync_Avatars_Interval": "Avatar Background Sync Interval", "LDAP_Background_Sync_Import_New_Users": "Background Sync Import New Users", "LDAP_Background_Sync_Import_New_Users_Description": "Will import all users (based on your filter criteria) that exists in LDAP and does not exists in Rocket.Chat", "LDAP_Background_Sync_Interval": "Background Sync Interval", @@ -2536,7 +2539,7 @@ "LDAP_Unique_Identifier_Field": "Unique Identifier Field", "LDAP_Unique_Identifier_Field_Description": "Which field will be used to link the LDAP user and the Rocket.Chat user. You can inform multiple values separated by comma to try to get the value from LDAP record.
Default value is `objectGUID,ibm-entryUUID,GUID,dominoUNID,nsuniqueId,uidNumber`", "LDAP_User_Search_AttributesToQuery": "Attributes to Query", - "LDAP_User_Search_AttributesToQuery_Description": "Specify which attributes should be returned on LDAP queries, separating them with commas. Defaults to everything. `*` represents all regular attributes and `+` represents all operational attributes.", + "LDAP_User_Search_AttributesToQuery_Description": "Specify which attributes should be returned on LDAP queries, separating them with commas. Defaults to everything. `*` represents all regular attributes and `+` represents all operational attributes. Make sure to include every attribute that is used by every Rocket.Chat sync option.", "LDAP_User_Search_Field": "Search Field", "LDAP_User_Search_Field_Description": "The LDAP attribute that identifies the LDAP user who attempts authentication. This field should be `sAMAccountName` for most Active Directory installations, but it may be `uid` for other LDAP solutions, such as OpenLDAP. You can use `mail` to identify users by email or whatever attribute you want.
You can use multiple values separated by comma to allow users to login using multiple identifiers like username or email.", "LDAP_User_Search_Filter": "Filter", diff --git a/server/lib/ldap/Manager.ts b/server/lib/ldap/Manager.ts index 5c036975e4d4..fac2e8438a95 100644 --- a/server/lib/ldap/Manager.ts +++ b/server/lib/ldap/Manager.ts @@ -67,6 +67,27 @@ export class LDAPManager { } } + public static syncUserAvatar(user: IUser, ldapUser: ILDAPEntry): void { + if (!user?._id || settings.get('LDAP_Sync_User_Avatar') !== true) { + return; + } + + const avatar = this.getAvatarFromUser(ldapUser); + if (!avatar) { + return; + } + + const hash = SHA256(avatar.toString()); + if (user.avatarHash === hash) { + return; + } + + logger.debug({ msg: 'Syncing user avatar', username: user.username }); + // #ToDo: Remove Meteor references here + // runAsUser is needed for now because the UploadFS class rejects files if there's no userId + Meteor.runAsUser(user._id, () => setUserAvatar(user, avatar, 'image/jpeg', 'rest')); + } + // This method will only find existing users that are already linked to LDAP protected static async findExistingLDAPUser(ldapUser: ILDAPEntry): Promise { const uniqueIdentifierField = this.getLdapUserUniqueID(ldapUser); @@ -384,20 +405,4 @@ export class LDAPManager { return ldapUser._raw.jpegPhoto; } } - - private static syncUserAvatar(user: IUser, ldapUser: ILDAPEntry): void { - if (!user?._id || settings.get('LDAP_Sync_User_Avatar') !== true) { - return; - } - - const avatar = this.getAvatarFromUser(ldapUser); - if (!avatar) { - return; - } - - logger.debug('Syncing user avatar'); - // #ToDo: Remove Meteor references here - // runAsUser is needed for now because the UploadFS class rejects files if there's no userId - Meteor.defer(() => Meteor.runAsUser(user._id, () => setUserAvatar(user, avatar, 'image/jpeg', 'rest'))); - } } diff --git a/server/settings/ldap.ts b/server/settings/ldap.ts index 3d772eaabd41..909373ccad52 100644 --- a/server/settings/ldap.ts +++ b/server/settings/ldap.ts @@ -231,6 +231,21 @@ settings.addGroup('LDAP', function() { enableQuery: backgroundSyncQuery, invalidValue: true, }); + + this.add('LDAP_Background_Sync_Avatars', false, { + type: 'boolean', + enableQuery, + invalidValue: false, + }); + + this.add('LDAP_Background_Sync_Avatars_Interval', 'Every 24 hours', { + type: 'string', + enableQuery: [ + enableQuery, + { _id: 'LDAP_Background_Sync_Avatars', value: true }, + ], + invalidValue: 'Every 24 hours', + }); }); this.section('LDAP_DataSync_Advanced', function() { From 34bd25f26d316191e9620da62d4c57217e76ad5a Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Wed, 22 Sep 2021 12:57:45 -0300 Subject: [PATCH 07/13] Moved EE settings back to EE folder --- ee/server/configuration/ldap.ts | 3 + ee/server/settings/index.ts | 1 + ee/server/settings/ldap.ts | 204 ++++++++++++++++++++++++++++++++ server/settings/ldap.ts | 197 ------------------------------ 4 files changed, 208 insertions(+), 197 deletions(-) create mode 100644 ee/server/settings/ldap.ts diff --git a/ee/server/configuration/ldap.ts b/ee/server/configuration/ldap.ts index 60b72eb7bc0f..2cfaa152b43b 100644 --- a/ee/server/configuration/ldap.ts +++ b/ee/server/configuration/ldap.ts @@ -12,8 +12,11 @@ import type { IImportUser } from '../../../definition/IImportUser'; import type { ILDAPEntry } from '../../../definition/ldap/ILDAPEntry'; import type { SettingCallback } from '../../../app/settings/lib/settings'; import { onLicense } from '../../app/license/server'; +import { addSettings } from '../settings/ldap'; onLicense('ldap-enterprise', () => { + addSettings(); + // Configure background sync cronjob function configureBackgroundSync(jobName: string, enableSetting: string, intervalSetting: string, cb: () => {}): SettingCallback { let lastSchedule: string; diff --git a/ee/server/settings/index.ts b/ee/server/settings/index.ts index c67512e1c280..c5dd94f6bac5 100644 --- a/ee/server/settings/index.ts +++ b/ee/server/settings/index.ts @@ -1 +1,2 @@ +import './ldap'; import './saml'; diff --git a/ee/server/settings/ldap.ts b/ee/server/settings/ldap.ts new file mode 100644 index 000000000000..bb8132ceaea3 --- /dev/null +++ b/ee/server/settings/ldap.ts @@ -0,0 +1,204 @@ +import { settings } from '../../../app/settings/server'; + +export function addSettings(): void { + settings.addGroup('LDAP', function() { + const enableQuery = { _id: 'LDAP_Enable', value: true }; + + this.set({ + tab: 'LDAP_Enterprise', + enterprise: true, + modules: ['ldap-enterprise'], + }, function() { + this.section('LDAP_DataSync_BackgroundSync', function() { + this.add('LDAP_Background_Sync', false, { + type: 'boolean', + enableQuery, + invalidValue: false, + }); + + const backgroundSyncQuery = [ + enableQuery, + { _id: 'LDAP_Background_Sync', value: true }, + ]; + + this.add('LDAP_Background_Sync_Interval', 'Every 24 hours', { + type: 'string', + enableQuery: backgroundSyncQuery, + invalidValue: 'Every 24 hours', + }); + + this.add('LDAP_Background_Sync_Import_New_Users', true, { + type: 'boolean', + enableQuery: backgroundSyncQuery, + invalidValue: true, + }); + + this.add('LDAP_Background_Sync_Keep_Existant_Users_Updated', true, { + type: 'boolean', + enableQuery: backgroundSyncQuery, + invalidValue: true, + }); + + this.add('LDAP_Background_Sync_Avatars', false, { + type: 'boolean', + enableQuery, + invalidValue: false, + }); + + this.add('LDAP_Background_Sync_Avatars_Interval', 'Every 24 hours', { + type: 'string', + enableQuery: [ + enableQuery, + { _id: 'LDAP_Background_Sync_Avatars', value: true }, + ], + invalidValue: 'Every 24 hours', + }); + }); + + this.section('LDAP_DataSync_Advanced', function() { + this.add('LDAP_Sync_User_Active_State', 'disable', { + type: 'select', + values: [ + { key: 'none', i18nLabel: 'LDAP_Sync_User_Active_State_Nothing' }, + { key: 'disable', i18nLabel: 'LDAP_Sync_User_Active_State_Disable' }, + { key: 'both', i18nLabel: 'LDAP_Sync_User_Active_State_Both' }, + ], + i18nDescription: 'LDAP_Sync_User_Active_State_Description', + enableQuery: { _id: 'LDAP_Enable', value: true }, + invalidValue: 'none', + }); + + this.add('LDAP_User_Search_AttributesToQuery', '*,+', { + type: 'string', + enableQuery: { _id: 'LDAP_Enable', value: true }, + invalidValue: '*,+', + }); + }); + + this.section('LDAP_DataSync_CustomFields', function() { + this.add('LDAP_Sync_Custom_Fields', false, { + type: 'boolean', + enableQuery, + invalidValue: false, + }); + + this.add('LDAP_CustomFieldMap', '{}', { + type: 'code', + multiline: true, + enableQuery: [ + enableQuery, + { _id: 'LDAP_Sync_Custom_Fields', value: true }, + ], + invalidValue: '{}', + }); + }); + + this.section('LDAP_DataSync_Roles', function() { + this.add('LDAP_Sync_User_Data_Roles', false, { + type: 'boolean', + enableQuery, + invalidValue: false, + }); + const syncRolesQuery = [ + enableQuery, + { _id: 'LDAP_Sync_User_Data_Roles', value: true }, + ]; + this.add('LDAP_Sync_User_Data_Roles_AutoRemove', false, { + type: 'boolean', + enableQuery: syncRolesQuery, + invalidValue: false, + }); + + this.add('LDAP_Sync_User_Data_Roles_Filter', '(&(cn=#{groupName})(memberUid=#{username}))', { + type: 'string', + enableQuery: syncRolesQuery, + invalidValue: '', + }); + + this.add('LDAP_Sync_User_Data_Roles_BaseDN', '', { + type: 'string', + enableQuery: syncRolesQuery, + invalidValue: '', + }); + + this.add('LDAP_Sync_User_Data_RolesMap', '{\n\t"rocket-admin": "admin",\n\t"tech-support": "support"\n}', { + type: 'code', + multiline: true, + public: false, + code: 'application/json', + enableQuery: syncRolesQuery, + invalidValue: '', + }); + }); + + this.section('LDAP_DataSync_Channels', function() { + this.add('LDAP_Sync_User_Data_Channels', false, { + type: 'boolean', + enableQuery, + invalidValue: false, + }); + + const syncChannelsQuery = [ + enableQuery, + { _id: 'LDAP_Sync_User_Data_Channels', value: true }, + ]; + + this.add('LDAP_Sync_User_Data_Channels_Admin', 'rocket.cat', { + type: 'string', + enableQuery: syncChannelsQuery, + invalidValue: 'rocket.cat', + }); + + this.add('LDAP_Sync_User_Data_Channels_Filter', '(&(cn=#{groupName})(memberUid=#{username}))', { + type: 'string', + enableQuery: syncChannelsQuery, + invalidValue: '', + }); + + this.add('LDAP_Sync_User_Data_Channels_BaseDN', '', { + type: 'string', + enableQuery: syncChannelsQuery, + invalidValue: '', + }); + + this.add('LDAP_Sync_User_Data_ChannelsMap', '{\n\t"employee": "general",\n\t"techsupport": [\n\t\t"helpdesk",\n\t\t"support"\n\t]\n}', { + type: 'code', + multiline: true, + public: false, + code: 'application/json', + enableQuery: syncChannelsQuery, + invalidValue: '', + }); + + this.add('LDAP_Sync_User_Data_Channels_Enforce_AutoChannels', false, { + type: 'boolean', + enableQuery: syncChannelsQuery, + invalidValue: false, + }); + }); + + this.section('LDAP_DataSync_Teams', function() { + this.add('LDAP_Enable_LDAP_Groups_To_RC_Teams', false, { + type: 'boolean', + enableQuery: { _id: 'LDAP_Enable', value: true }, + invalidValue: false, + }); + this.add('LDAP_Groups_To_Rocket_Chat_Teams', '{}', { + type: 'code', + enableQuery: { _id: 'LDAP_Enable_LDAP_Groups_To_RC_Teams', value: true }, + invalidValue: '{}', + }); + this.add('LDAP_Validate_Teams_For_Each_Login', false, { + type: 'boolean', + enableQuery: { _id: 'LDAP_Enable_LDAP_Groups_To_RC_Teams', value: true }, + invalidValue: false, + }); + this.add('LDAP_Query_To_Get_User_Teams', '(&(ou=*)(uniqueMember=uid=#{username},dc=example,dc=com))', { + type: 'string', + enableQuery: { _id: 'LDAP_Enable_LDAP_Groups_To_RC_Teams', value: true }, + invalidValue: '(&(ou=*)(uniqueMember=uid=#{username},dc=example,dc=com))', + }); + }); + }); + }); +} diff --git a/server/settings/ldap.ts b/server/settings/ldap.ts index 909373ccad52..cb9d235634ae 100644 --- a/server/settings/ldap.ts +++ b/server/settings/ldap.ts @@ -196,201 +196,4 @@ settings.addGroup('LDAP', function() { }); }); }); - - this.set({ - tab: 'LDAP_Enterprise', - enterprise: true, - modules: ['ldap-enterprise'], - }, function() { - this.section('LDAP_DataSync_BackgroundSync', function() { - this.add('LDAP_Background_Sync', false, { - type: 'boolean', - enableQuery, - invalidValue: false, - }); - - const backgroundSyncQuery = [ - enableQuery, - { _id: 'LDAP_Background_Sync', value: true }, - ]; - - this.add('LDAP_Background_Sync_Interval', 'Every 24 hours', { - type: 'string', - enableQuery: backgroundSyncQuery, - invalidValue: 'Every 24 hours', - }); - - this.add('LDAP_Background_Sync_Import_New_Users', true, { - type: 'boolean', - enableQuery: backgroundSyncQuery, - invalidValue: true, - }); - - this.add('LDAP_Background_Sync_Keep_Existant_Users_Updated', true, { - type: 'boolean', - enableQuery: backgroundSyncQuery, - invalidValue: true, - }); - - this.add('LDAP_Background_Sync_Avatars', false, { - type: 'boolean', - enableQuery, - invalidValue: false, - }); - - this.add('LDAP_Background_Sync_Avatars_Interval', 'Every 24 hours', { - type: 'string', - enableQuery: [ - enableQuery, - { _id: 'LDAP_Background_Sync_Avatars', value: true }, - ], - invalidValue: 'Every 24 hours', - }); - }); - - this.section('LDAP_DataSync_Advanced', function() { - this.add('LDAP_Sync_User_Active_State', 'disable', { - type: 'select', - values: [ - { key: 'none', i18nLabel: 'LDAP_Sync_User_Active_State_Nothing' }, - { key: 'disable', i18nLabel: 'LDAP_Sync_User_Active_State_Disable' }, - { key: 'both', i18nLabel: 'LDAP_Sync_User_Active_State_Both' }, - ], - i18nDescription: 'LDAP_Sync_User_Active_State_Description', - enableQuery: { _id: 'LDAP_Enable', value: true }, - invalidValue: 'none', - }); - - this.add('LDAP_User_Search_AttributesToQuery', '*,+', { - type: 'string', - enableQuery: { _id: 'LDAP_Enable', value: true }, - invalidValue: '*,+', - }); - }); - - this.section('LDAP_DataSync_CustomFields', function() { - this.add('LDAP_Sync_Custom_Fields', false, { - type: 'boolean', - enableQuery, - invalidValue: false, - }); - - this.add('LDAP_CustomFieldMap', '{}', { - type: 'code', - multiline: true, - enableQuery: [ - enableQuery, - { _id: 'LDAP_Sync_Custom_Fields', value: true }, - ], - invalidValue: '{}', - }); - }); - - this.section('LDAP_DataSync_Roles', function() { - this.add('LDAP_Sync_User_Data_Roles', false, { - type: 'boolean', - enableQuery, - invalidValue: false, - }); - const syncRolesQuery = [ - enableQuery, - { _id: 'LDAP_Sync_User_Data_Roles', value: true }, - ]; - this.add('LDAP_Sync_User_Data_Roles_AutoRemove', false, { - type: 'boolean', - enableQuery: syncRolesQuery, - invalidValue: false, - }); - - this.add('LDAP_Sync_User_Data_Roles_Filter', '(&(cn=#{groupName})(memberUid=#{username}))', { - type: 'string', - enableQuery: syncRolesQuery, - invalidValue: '', - }); - - this.add('LDAP_Sync_User_Data_Roles_BaseDN', '', { - type: 'string', - enableQuery: syncRolesQuery, - invalidValue: '', - }); - - this.add('LDAP_Sync_User_Data_RolesMap', '{\n\t"rocket-admin": "admin",\n\t"tech-support": "support"\n}', { - type: 'code', - multiline: true, - public: false, - code: 'application/json', - enableQuery: syncRolesQuery, - invalidValue: '', - }); - }); - - this.section('LDAP_DataSync_Channels', function() { - this.add('LDAP_Sync_User_Data_Channels', false, { - type: 'boolean', - enableQuery, - invalidValue: false, - }); - - const syncChannelsQuery = [ - enableQuery, - { _id: 'LDAP_Sync_User_Data_Channels', value: true }, - ]; - - this.add('LDAP_Sync_User_Data_Channels_Admin', 'rocket.cat', { - type: 'string', - enableQuery: syncChannelsQuery, - invalidValue: 'rocket.cat', - }); - - this.add('LDAP_Sync_User_Data_Channels_Filter', '(&(cn=#{groupName})(memberUid=#{username}))', { - type: 'string', - enableQuery: syncChannelsQuery, - invalidValue: '', - }); - - this.add('LDAP_Sync_User_Data_Channels_BaseDN', '', { - type: 'string', - enableQuery: syncChannelsQuery, - invalidValue: '', - }); - - this.add('LDAP_Sync_User_Data_ChannelsMap', '{\n\t"employee": "general",\n\t"techsupport": [\n\t\t"helpdesk",\n\t\t"support"\n\t]\n}', { - type: 'code', - multiline: true, - public: false, - code: 'application/json', - enableQuery: syncChannelsQuery, - invalidValue: '', - }); - - this.add('LDAP_Sync_User_Data_Channels_Enforce_AutoChannels', false, { - type: 'boolean', - enableQuery: syncChannelsQuery, - invalidValue: false, - }); - }); - - this.section('LDAP_DataSync_Teams', function() { - this.add('LDAP_Enable_LDAP_Groups_To_RC_Teams', false, { - type: 'boolean', - enableQuery: { _id: 'LDAP_Enable', value: true }, - invalidValue: false, - }); - this.add('LDAP_Groups_To_Rocket_Chat_Teams', '{}', { - type: 'code', - enableQuery: { _id: 'LDAP_Enable_LDAP_Groups_To_RC_Teams', value: true }, - invalidValue: '{}', - }); - this.add('LDAP_Validate_Teams_For_Each_Login', false, { - type: 'boolean', - enableQuery: { _id: 'LDAP_Enable_LDAP_Groups_To_RC_Teams', value: true }, - invalidValue: false, - }); - this.add('LDAP_Query_To_Get_User_Teams', '(&(ou=*)(uniqueMember=uid=#{username},dc=example,dc=com))', { - type: 'string', - enableQuery: { _id: 'LDAP_Enable_LDAP_Groups_To_RC_Teams', value: true }, - invalidValue: '(&(ou=*)(uniqueMember=uid=#{username},dc=example,dc=com))', - }); - }); - }); }); From 404805c003e63926f3159f99cb624f6106b5fe3e Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Wed, 22 Sep 2021 18:54:56 -0300 Subject: [PATCH 08/13] Added additional checks for determining if an user is deactivated on LDAP --- ee/server/lib/ldap/Manager.ts | 38 ++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/ee/server/lib/ldap/Manager.ts b/ee/server/lib/ldap/Manager.ts index d7669c63200d..fc2e434ccffe 100644 --- a/ee/server/lib/ldap/Manager.ts +++ b/ee/server/lib/ldap/Manager.ts @@ -353,6 +353,42 @@ export class LDAPEEManager extends LDAPManager { return ldapUserGroups.filter((entry) => entry?.raw?.ou).map((entry) => (ldap.extractLdapAttribute(entry.raw.ou) as string)).flat(); } + private static isUserDeactivated(ldapUser: ILDAPEntry): boolean { + // Account locked by "Draft-behera-ldap-password-policy" + if (ldapUser.pwdAccountLockedTime) { + return true; + } + + // EDirectory: Account manually disabled by an admin + if (ldapUser.loginDisabled) { + return true; + } + + // Oracle: Account must not be allowed to authenticate + if (ldapUser.orclIsEnabled && ldapUser.orclIsEnabled !== 'ENABLED') { + return true; + } + + // Active Directory - Account locked automatically by security policies + if (ldapUser.lockoutTime) { + // Automatic unlock is disabled + if (!ldapUser.lockoutDuration) { + return true; + } + + // #ToDo: Account for timezones + const lockoutTime = new Date(Number(ldapUser.lockoutTime)); + lockoutTime.setMinutes(lockoutTime.getMinutes() + Number(ldapUser.lockoutDuration)); + // Account has not unlocked itself yet + if (lockoutTime.valueOf() > Date.now()) { + return true; + } + } + + // #ToDo: Active Directory - Account disabled by an Admin + return false; + } + public static copyActiveState(ldapUser: ILDAPEntry, userData: IImportUser): void { if (!ldapUser) { return; @@ -363,7 +399,7 @@ export class LDAPEEManager extends LDAPManager { return; } - const deleted = Boolean(ldapUser.pwdAccountLockedTime); + const deleted = this.isUserDeactivated(ldapUser); if (deleted === userData.deleted || (userData.deleted === undefined && !deleted)) { return; } From 21ced4b76761992a2617c9e7e4c1615c81d0d3c1 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 23 Sep 2021 16:46:11 -0300 Subject: [PATCH 09/13] Fix review --- ee/server/lib/ldap/Manager.ts | 82 ++++++++++++++--------------------- 1 file changed, 33 insertions(+), 49 deletions(-) diff --git a/ee/server/lib/ldap/Manager.ts b/ee/server/lib/ldap/Manager.ts index fc2e434ccffe..1d1db580ab0e 100644 --- a/ee/server/lib/ldap/Manager.ts +++ b/ee/server/lib/ldap/Manager.ts @@ -520,62 +520,46 @@ export class LDAPEEManager extends LDAPManager { } private static async updateExistingUsers(ldap: LDAPConnection, converter: LDAPDataConverter): Promise { - return new Promise(async (resolve, reject) => { - try { - const users = await UsersRaw.findLDAPUsers(); - for await (const user of users) { - let ldapUser: ILDAPEntry | undefined; - - if (user.services?.ldap?.id) { - ldapUser = await ldap.findOneById(user.services.ldap.id, user.services.ldap.idAttribute); - } else if (user.username) { - ldapUser = await ldap.findOneByUsername(user.username); - } - - if (ldapUser) { - const userData = this.mapUserData(ldapUser, user.username); - converter.addUser(userData); - } - } + const users = await UsersRaw.findLDAPUsers(); + for await (const user of users) { + let ldapUser: ILDAPEntry | undefined; + + if (user.services?.ldap?.id) { + ldapUser = await ldap.findOneById(user.services.ldap.id, user.services.ldap.idAttribute); + } else if (user.username) { + ldapUser = await ldap.findOneByUsername(user.username); + } - resolve(); - } catch (error) { - reject(error); + if (ldapUser) { + const userData = this.mapUserData(ldapUser, user.username); + converter.addUser(userData); } - }); + } } private static async updateUserAvatars(ldap: LDAPConnection): Promise { - return new Promise(async (resolve, reject) => { - try { - const users = await UsersRaw.findLDAPUsers(); - for await (const user of users) { - let ldapUser: ILDAPEntry | undefined; - - if (user.services?.ldap?.id) { - ldapUser = await ldap.findOneById(user.services.ldap.id, user.services.ldap.idAttribute); - } else if (user.username) { - ldapUser = await ldap.findOneByUsername(user.username); - } - - if (!ldapUser) { - searchLogger.debug({ - msg: 'existing LDAP user not found during Avatar Sync', - ldapId: user.services?.ldap?.id, - ldapAttribute: user.services?.ldap?.idAttribute, - username: user.username, - }); - continue; - } - - LDAPManager.syncUserAvatar(user, ldapUser); - } + const users = await UsersRaw.findLDAPUsers(); + for await (const user of users) { + let ldapUser: ILDAPEntry | undefined; + + if (user.services?.ldap?.id) { + ldapUser = await ldap.findOneById(user.services.ldap.id, user.services.ldap.idAttribute); + } else if (user.username) { + ldapUser = await ldap.findOneByUsername(user.username); + } - resolve(); - } catch (error) { - reject(error); + if (!ldapUser) { + searchLogger.debug({ + msg: 'existing LDAP user not found during Avatar Sync', + ldapId: user.services?.ldap?.id, + ldapAttribute: user.services?.ldap?.idAttribute, + username: user.username, + }); + continue; } - }); + + LDAPManager.syncUserAvatar(user, ldapUser); + } } private static getCustomField(customFields: Record, property: string): any { From add1f9b947cbb88c6d51cd7cf656bd11ad1e7633 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Thu, 23 Sep 2021 19:05:05 -0300 Subject: [PATCH 10/13] Auto logout --- app/models/server/raw/Users.js | 12 +++++ ee/server/configuration/ldap.ts | 18 ++++++-- ee/server/lib/ldap/Manager.ts | 57 +++++++++++++++++++++++- ee/server/local-services/ldap/service.ts | 4 ++ ee/server/sdk/types/ILDAPEEService.ts | 1 + ee/server/settings/ldap.ts | 17 +++++++ 6 files changed, 103 insertions(+), 6 deletions(-) diff --git a/app/models/server/raw/Users.js b/app/models/server/raw/Users.js index f18b67309a1a..7ed58ba760be 100644 --- a/app/models/server/raw/Users.js +++ b/app/models/server/raw/Users.js @@ -156,6 +156,18 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } + async findConnectedLDAPUsers(options) { + const query = { + ldap: true, + 'service.resume.loginTokens': { + $exists: true, + $ne: [], + }, + }; + + return this.find(query, options); + } + isUserInRole(userId, roleName) { const query = { _id: userId, diff --git a/ee/server/configuration/ldap.ts b/ee/server/configuration/ldap.ts index 2cfaa152b43b..a7071354973c 100644 --- a/ee/server/configuration/ldap.ts +++ b/ee/server/configuration/ldap.ts @@ -10,11 +10,12 @@ import { LDAPEEManager } from '../lib/ldap/Manager'; import { callbacks } from '../../../app/callbacks/server'; import type { IImportUser } from '../../../definition/IImportUser'; import type { ILDAPEntry } from '../../../definition/ldap/ILDAPEntry'; +import type { SettingValue } from '../../../definition/ISetting'; import type { SettingCallback } from '../../../app/settings/lib/settings'; import { onLicense } from '../../app/license/server'; import { addSettings } from '../settings/ldap'; -onLicense('ldap-enterprise', () => { +Meteor.startup(() => onLicense('ldap-enterprise', () => { addSettings(); // Configure background sync cronjob @@ -22,9 +23,9 @@ onLicense('ldap-enterprise', () => { let lastSchedule: string; return _.debounce(Meteor.bindEnvironment(function addCronJobDebounced() { - if (settings.get(enableSetting) !== true) { - logger.info({ msg: 'Disabling LDAP Background Sync', jobName }); + if (settings.get('LDAP_Enable') !== true || settings.get(enableSetting) !== true) { if (cronJobs.nextScheduledAtDate(jobName)) { + logger.info({ msg: 'Disabling LDAP Background Sync', jobName }); cronJobs.remove(jobName); } return; @@ -45,12 +46,21 @@ onLicense('ldap-enterprise', () => { const addCronJob = configureBackgroundSync('LDAP_Sync', 'LDAP_Background_Sync', 'LDAP_Background_Sync_Interval', () => Promise.await(LDAPEE.sync())); const addAvatarCronJob = configureBackgroundSync('LDAP_AvatarSync', 'LDAP_Background_Sync_Avatars', 'LDAP_Background_Sync_Avatars_Interval', () => Promise.await(LDAPEE.syncAvatars())); + const addLogoutCronJob = configureBackgroundSync('LDAP_AutoLogout', 'LDAP_Sync_AutoLogout_Enabled', 'LDAP_Sync_AutoLogout_Interval', () => Promise.await(LDAPEE.syncLogout())); Meteor.defer(() => { settings.get('LDAP_Background_Sync', addCronJob); settings.get('LDAP_Background_Sync_Interval', addCronJob); settings.get('LDAP_Background_Sync_Avatars', addAvatarCronJob); settings.get('LDAP_Background_Sync_Avatars_Interval', addAvatarCronJob); + settings.get('LDAP_Sync_AutoLogout_Enabled', addLogoutCronJob); + settings.get('LDAP_Sync_AutoLogout_Interval', addLogoutCronJob); + + settings.get('LDAP_Enable', (key: string, value: SettingValue, initialLoad?: boolean) => { + addCronJob(key, value, initialLoad); + addAvatarCronJob(key, value, initialLoad); + addLogoutCronJob(key, value, initialLoad); + }); settings.get('LDAP_Groups_To_Rocket_Chat_Teams', (_key, value) => { try { @@ -65,4 +75,4 @@ onLicense('ldap-enterprise', () => { LDAPEEManager.copyCustomFields(ldapUser, userData); LDAPEEManager.copyActiveState(ldapUser, userData); }, callbacks.priority.MEDIUM, 'mapLDAPCustomFields'); -}); +})); diff --git a/ee/server/lib/ldap/Manager.ts b/ee/server/lib/ldap/Manager.ts index fc2e434ccffe..0afee608007d 100644 --- a/ee/server/lib/ldap/Manager.ts +++ b/ee/server/lib/ldap/Manager.ts @@ -94,6 +94,25 @@ export class LDAPEEManager extends LDAPManager { } } + public static async syncLogout(): Promise { + if (settings.get('LDAP_Enable') !== true || settings.get('LDAP_Background_Sync_Avatars') !== true) { + return; + } + + try { + const ldap = new LDAPConnection(); + await ldap.connect(); + + try { + await this.logoutDeactivatedUsers(ldap); + } finally { + ldap.disconnect(); + } + } catch (error) { + logger.error(error); + } + } + private static async advancedSync(ldap: LDAPConnection, importUser: IImportUser, converter: LDAPDataConverter, isNewRecord: boolean): Promise { const user = converter.findExistingUser(importUser); if (!user || user.username) { @@ -376,7 +395,6 @@ export class LDAPEEManager extends LDAPManager { return true; } - // #ToDo: Account for timezones const lockoutTime = new Date(Number(ldapUser.lockoutTime)); lockoutTime.setMinutes(lockoutTime.getMinutes() + Number(ldapUser.lockoutDuration)); // Account has not unlocked itself yet @@ -385,7 +403,11 @@ export class LDAPEEManager extends LDAPManager { } } - // #ToDo: Active Directory - Account disabled by an Admin + // Active Directory - Account disabled by an Admin + if (ldapUser.userAccountControl && (ldapUser.userAccountControl & 2) === 2) { + return true; + } + return false; } @@ -578,6 +600,37 @@ export class LDAPEEManager extends LDAPManager { }); } + private static async findLDAPUser(ldap: LDAPConnection, user: IUser): Promise { + if (user.services?.ldap?.id) { + return ldap.findOneById(user.services.ldap.id, user.services.ldap.idAttribute); + } + + if (user.username) { + return ldap.findOneByUsername(user.username); + } + + searchLogger.debug({ + msg: 'existing LDAP user not found during Sync', + ldapId: user.services?.ldap?.id, + ldapAttribute: user.services?.ldap?.idAttribute, + username: user.username, + }); + } + + private static async logoutDeactivatedUsers(ldap: LDAPConnection): Promise { + const users = await UsersRaw.findConnectedLDAPUsers(); + for await (const user of users) { + const ldapUser = await this.findLDAPUser(ldap, user); + if (!ldapUser) { + continue; + } + + if (this.isUserDeactivated(ldapUser)) { + UsersRaw.removeResumeService(user._id); + } + } + } + private static getCustomField(customFields: Record, property: string): any { try { return _.reduce(property.split('.'), (acc, el) => acc[el], customFields); diff --git a/ee/server/local-services/ldap/service.ts b/ee/server/local-services/ldap/service.ts index 14227ae9a7a0..977f8d5a0055 100644 --- a/ee/server/local-services/ldap/service.ts +++ b/ee/server/local-services/ldap/service.ts @@ -15,6 +15,10 @@ export class LDAPEEService extends ServiceClass implements ILDAPEEService { async syncAvatars(): Promise { return LDAPEEManager.syncAvatars(); } + + async syncLogout(): Promise { + return LDAPEEManager.syncLogout(); + } } api.registerService(new LDAPEEService()); diff --git a/ee/server/sdk/types/ILDAPEEService.ts b/ee/server/sdk/types/ILDAPEEService.ts index cadc6eedc3a7..246d52884a8f 100644 --- a/ee/server/sdk/types/ILDAPEEService.ts +++ b/ee/server/sdk/types/ILDAPEEService.ts @@ -1,4 +1,5 @@ export interface ILDAPEEService { sync(): Promise; syncAvatars(): Promise; + syncLogout(): Promise; } diff --git a/ee/server/settings/ldap.ts b/ee/server/settings/ldap.ts index bb8132ceaea3..af172127683a 100644 --- a/ee/server/settings/ldap.ts +++ b/ee/server/settings/ldap.ts @@ -75,6 +75,23 @@ export function addSettings(): void { }); }); + this.section('LDAP_DataSync_AutoLogout', function() { + this.add('LDAP_Sync_AutoLogout_Enabled', false, { + type: 'boolean', + enableQuery, + invalidValue: false, + }); + + this.add('LDAP_Sync_AutoLogout_Interval', 'Every 5 minutes', { + type: 'string', + enableQuery: [ + enableQuery, + { _id: 'LDAP_Sync_AutoLogout_Enabled', value: true }, + ], + invalidValue: '', + }); + }); + this.section('LDAP_DataSync_CustomFields', function() { this.add('LDAP_Sync_Custom_Fields', false, { type: 'boolean', From a676070dadf0c322946ab2777da4c1eef7f44139 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Thu, 23 Sep 2021 19:57:23 -0300 Subject: [PATCH 11/13] Auto logout --- app/models/server/raw/Users.js | 6 +++--- ee/server/lib/ldap/Manager.ts | 34 ++++++++-------------------------- 2 files changed, 11 insertions(+), 29 deletions(-) diff --git a/app/models/server/raw/Users.js b/app/models/server/raw/Users.js index 82b8ae7d414e..a021a91f25e6 100644 --- a/app/models/server/raw/Users.js +++ b/app/models/server/raw/Users.js @@ -150,16 +150,16 @@ export class UsersRaw extends BaseRaw { return this.findOne(query); } - async findLDAPUsers(options) { + findLDAPUsers(options) { const query = { ldap: true }; return this.find(query, options); } - async findConnectedLDAPUsers(options) { + findConnectedLDAPUsers(options) { const query = { ldap: true, - 'service.resume.loginTokens': { + 'services.resume.loginTokens': { $exists: true, $ne: [], }, diff --git a/ee/server/lib/ldap/Manager.ts b/ee/server/lib/ldap/Manager.ts index 9c89d32bd0da..4893c2c3e2d0 100644 --- a/ee/server/lib/ldap/Manager.ts +++ b/ee/server/lib/ldap/Manager.ts @@ -95,7 +95,7 @@ export class LDAPEEManager extends LDAPManager { } public static async syncLogout(): Promise { - if (settings.get('LDAP_Enable') !== true || settings.get('LDAP_Background_Sync_Avatars') !== true) { + if (settings.get('LDAP_Enable') !== true || settings.get('LDAP_Sync_AutoLogout_Enabled') !== true) { return; } @@ -542,15 +542,9 @@ export class LDAPEEManager extends LDAPManager { } private static async updateExistingUsers(ldap: LDAPConnection, converter: LDAPDataConverter): Promise { - const users = await UsersRaw.findLDAPUsers(); + const users = await UsersRaw.findLDAPUsers().toArray(); for await (const user of users) { - let ldapUser: ILDAPEntry | undefined; - - if (user.services?.ldap?.id) { - ldapUser = await ldap.findOneById(user.services.ldap.id, user.services.ldap.idAttribute); - } else if (user.username) { - ldapUser = await ldap.findOneByUsername(user.username); - } + const ldapUser = await this.findLDAPUser(ldap, user); if (ldapUser) { const userData = this.mapUserData(ldapUser, user.username); @@ -560,23 +554,10 @@ export class LDAPEEManager extends LDAPManager { } private static async updateUserAvatars(ldap: LDAPConnection): Promise { - const users = await UsersRaw.findLDAPUsers(); + const users = await UsersRaw.findLDAPUsers().toArray(); for await (const user of users) { - let ldapUser: ILDAPEntry | undefined; - - if (user.services?.ldap?.id) { - ldapUser = await ldap.findOneById(user.services.ldap.id, user.services.ldap.idAttribute); - } else if (user.username) { - ldapUser = await ldap.findOneByUsername(user.username); - } - + const ldapUser = await this.findLDAPUser(ldap, user); if (!ldapUser) { - searchLogger.debug({ - msg: 'existing LDAP user not found during Avatar Sync', - ldapId: user.services?.ldap?.id, - ldapAttribute: user.services?.ldap?.idAttribute, - username: user.username, - }); continue; } @@ -602,7 +583,8 @@ export class LDAPEEManager extends LDAPManager { } private static async logoutDeactivatedUsers(ldap: LDAPConnection): Promise { - const users = await UsersRaw.findConnectedLDAPUsers(); + const users = await UsersRaw.findConnectedLDAPUsers().toArray(); + for await (const user of users) { const ldapUser = await this.findLDAPUser(ldap, user); if (!ldapUser) { @@ -610,7 +592,7 @@ export class LDAPEEManager extends LDAPManager { } if (this.isUserDeactivated(ldapUser)) { - UsersRaw.removeResumeService(user._id); + UsersRaw.unsetLoginTokens(user._id); } } } From aa458c3787d19485ac23e644b4242ca11ed2a70a Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Thu, 23 Sep 2021 19:58:05 -0300 Subject: [PATCH 12/13] Store avatar hash as etag --- app/lib/server/functions/setUserAvatar.js | 8 +++----- app/models/server/models/Users.js | 3 +-- definition/IUser.ts | 1 - server/lib/ldap/Manager.ts | 4 ++-- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/app/lib/server/functions/setUserAvatar.js b/app/lib/server/functions/setUserAvatar.js index 1a39045eb51d..50b5e130dc9c 100644 --- a/app/lib/server/functions/setUserAvatar.js +++ b/app/lib/server/functions/setUserAvatar.js @@ -1,6 +1,5 @@ import { Meteor } from 'meteor/meteor'; import { HTTP } from 'meteor/http'; -import { SHA256 } from 'meteor/sha'; import { RocketChatFile } from '../../../file/server'; import { FileUpload } from '../../../file-upload/server'; @@ -8,7 +7,7 @@ import { Users } from '../../../models/server'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { api } from '../../../../server/sdk/api'; -export const setUserAvatar = function(user, dataURI, contentType, service) { +export const setUserAvatar = function(user, dataURI, contentType, service, etag = null) { let encoding; let image; @@ -54,7 +53,6 @@ export const setUserAvatar = function(user, dataURI, contentType, service) { } const buffer = Buffer.from(image, encoding); - const hash = SHA256(buffer.toString()); const fileStore = FileUpload.getStore('Avatars'); fileStore.deleteByName(user.username); @@ -67,8 +65,8 @@ export const setUserAvatar = function(user, dataURI, contentType, service) { fileStore.insert(file, buffer, (err, result) => { Meteor.setTimeout(function() { - Users.setAvatarData(user._id, service, result.etag, hash); - api.broadcast('user.avatarUpdate', { username: user.username, avatarETag: result.etag }); + Users.setAvatarData(user._id, service, etag || result.etag); + api.broadcast('user.avatarUpdate', { username: user.username, avatarETag: etag || result.etag }); }, 500); }); }; diff --git a/app/models/server/models/Users.js b/app/models/server/models/Users.js index df0546e5a662..0a868927acae 100644 --- a/app/models/server/models/Users.js +++ b/app/models/server/models/Users.js @@ -1145,12 +1145,11 @@ export class Users extends Base { return this.update(_id, update); } - setAvatarData(_id, origin, etag, hash = null) { + setAvatarData(_id, origin, etag) { const update = { $set: { avatarOrigin: origin, avatarETag: etag, - avatarHash: hash, }, }; diff --git a/definition/IUser.ts b/definition/IUser.ts index c3047482d8bf..732c684961ba 100644 --- a/definition/IUser.ts +++ b/definition/IUser.ts @@ -121,7 +121,6 @@ export interface IUser extends IRocketChatRecord { lastLogin?: Date; avatarOrigin?: string; avatarETag?: string; - avatarHash?: string; utcOffset?: number; language?: string; statusDefault?: UserStatus; diff --git a/server/lib/ldap/Manager.ts b/server/lib/ldap/Manager.ts index fac2e8438a95..d7cc6cd5d4d1 100644 --- a/server/lib/ldap/Manager.ts +++ b/server/lib/ldap/Manager.ts @@ -78,14 +78,14 @@ export class LDAPManager { } const hash = SHA256(avatar.toString()); - if (user.avatarHash === hash) { + if (user.avatarETag === hash) { return; } logger.debug({ msg: 'Syncing user avatar', username: user.username }); // #ToDo: Remove Meteor references here // runAsUser is needed for now because the UploadFS class rejects files if there's no userId - Meteor.runAsUser(user._id, () => setUserAvatar(user, avatar, 'image/jpeg', 'rest')); + Meteor.runAsUser(user._id, () => setUserAvatar(user, avatar, 'image/jpeg', 'rest', hash)); } // This method will only find existing users that are already linked to LDAP From 174270744bcd16740ca280373a9b1ea68d3090f5 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Thu, 23 Sep 2021 20:10:12 -0300 Subject: [PATCH 13/13] review fixes --- app/lib/server/functions/setUserAvatar.js | 3 +-- packages/rocketchat-i18n/i18n/en.i18n.json | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/lib/server/functions/setUserAvatar.js b/app/lib/server/functions/setUserAvatar.js index 50b5e130dc9c..8fa18240cf18 100644 --- a/app/lib/server/functions/setUserAvatar.js +++ b/app/lib/server/functions/setUserAvatar.js @@ -12,7 +12,7 @@ export const setUserAvatar = function(user, dataURI, contentType, service, etag let image; if (service === 'initials') { - return Users.setAvatarData(user._id, service, null, null); + return Users.setAvatarData(user._id, service, null); } if (service === 'url') { let result = null; @@ -53,7 +53,6 @@ export const setUserAvatar = function(user, dataURI, contentType, service, etag } const buffer = Buffer.from(image, encoding); - const fileStore = FileUpload.getStore('Avatars'); fileStore.deleteByName(user.username); diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 53cb7dd32b40..124f02b69b09 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -2510,6 +2510,8 @@ "LDAP_Search_Size_Limit_Description": "The maximum number of entries to return.
**Attention** This number should greater than **Search Page Size**", "LDAP_Sync_Custom_Fields": "Sync Custom Fields", "LDAP_CustomFieldMap": "Custom Fields Mapping", + "LDAP_Sync_AutoLogout_Enabled": "Enable Auto Logout", + "LDAP_Sync_AutoLogout_Interval": "Auto Logout Interval", "LDAP_Sync_Now": "Sync Now", "LDAP_Sync_Now_Description": "This will start a **Background Sync** operation now, without waiting for the next scheduled Sync.\nThis action is asynchronous, please see the logs for more information.", "LDAP_Sync_User_Active_State": "Sync User Active State",