The communications platform that puts data protection first.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
Rocket.Chat/app/ldap/server/sync.js

605 lines
16 KiB

import limax from 'limax';
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import { SyncedCron } from 'meteor/littledata:synced-cron';
import _ from 'underscore';
import LDAP from './ldap';
import { RocketChatFile } from '../../file';
import { settings } from '../../settings';
import { Notifications } from '../../notifications';
import { Users, Roles, Rooms, Subscriptions } from '../../models';
import { Logger } from '../../logger';
import { _setRealName, _setUsername } from '../../lib';
import { templateVarHandler } from '../../utils';
import { FileUpload } from '../../file-upload';
import { addUserToRoom, removeUserFromRoom, createRoom } from '../../lib/server/functions';
const logger = new Logger('LDAPSync', {});
export function isUserInLDAPGroup(ldap, ldapUser, user, ldapGroup) {
const syncUserRolesFilter = settings.get('LDAP_Sync_User_Data_Groups_Filter').trim();
const syncUserRolesBaseDN = settings.get('LDAP_Sync_User_Data_Groups_BaseDN').trim();
if (!syncUserRolesFilter || !syncUserRolesBaseDN) {
logger.error('Please setup LDAP Group Filter and LDAP Group BaseDN in LDAP Settings.');
return false;
}
const searchOptions = {
filter: syncUserRolesFilter.replace(/#{username}/g, user.username).replace(/#{groupName}/g, ldapGroup),
scope: 'sub',
};
const result = ldap.searchAllSync(syncUserRolesBaseDN, searchOptions);
if (!Array.isArray(result) || result.length === 0) {
logger.debug(`${ user.username } is not in ${ ldapGroup } group!!!`);
} else {
logger.debug(`${ user.username } is in ${ ldapGroup } group.`);
return true;
}
return false;
}
export function slug(text) {
if (settings.get('UTF8_Names_Slugify') !== true) {
return text;
}
text = limax(text, { replacement: '.', separateNumbers: false });
return text.replace(/[^0-9a-z-_.]/g, '');
}
export function getPropertyValue(obj, key) {
try {
return _.reduce(key.split('.'), (acc, el) => acc[el], obj);
} catch (err) {
return undefined;
}
}
export function getLdapUsername(ldapUser) {
const usernameField = settings.get('LDAP_Username_Field');
if (usernameField.indexOf('#{') > -1) {
return usernameField.replace(/#{(.+?)}/g, function(match, field) {
return ldapUser[field];
});
}
return ldapUser[usernameField];
}
export function getLdapUserUniqueID(ldapUser) {
let Unique_Identifier_Field = settings.get('LDAP_Unique_Identifier_Field');
if (Unique_Identifier_Field !== '') {
Unique_Identifier_Field = Unique_Identifier_Field.replace(/\s/g, '').split(',');
} else {
Unique_Identifier_Field = [];
}
let User_Search_Field = settings.get('LDAP_User_Search_Field');
if (User_Search_Field !== '') {
User_Search_Field = User_Search_Field.replace(/\s/g, '').split(',');
} else {
User_Search_Field = [];
}
Unique_Identifier_Field = Unique_Identifier_Field.concat(User_Search_Field);
if (Unique_Identifier_Field.length > 0) {
Unique_Identifier_Field = Unique_Identifier_Field.find((field) => !_.isEmpty(ldapUser._raw[field]));
if (Unique_Identifier_Field) {
Unique_Identifier_Field = {
attribute: Unique_Identifier_Field,
value: ldapUser._raw[Unique_Identifier_Field].toString('hex'),
};
}
return Unique_Identifier_Field;
}
}
export function getDataToSyncUserData(ldapUser, user) {
const syncUserData = settings.get('LDAP_Sync_User_Data');
const syncUserDataFieldMap = settings.get('LDAP_Sync_User_Data_FieldMap').trim();
const userData = {};
if (syncUserData && syncUserDataFieldMap) {
const whitelistedUserFields = ['email', 'name', 'customFields'];
const fieldMap = JSON.parse(syncUserDataFieldMap);
const emailList = [];
_.map(fieldMap, function(userField, ldapField) {
switch (userField) {
case 'email':
if (!ldapUser.hasOwnProperty(ldapField)) {
logger.debug(`user does not have attribute: ${ ldapField }`);
return;
}
if (_.isObject(ldapUser[ldapField])) {
_.map(ldapUser[ldapField], function(item) {
emailList.push({ address: item, verified: true });
});
} else {
emailList.push({ address: ldapUser[ldapField], verified: true });
}
break;
default:
const [outerKey, innerKeys] = userField.split(/\.(.+)/);
if (!_.find(whitelistedUserFields, (el) => el === outerKey)) {
logger.debug(`user attribute not whitelisted: ${ userField }`);
return;
}
if (outerKey === 'customFields') {
let customFieldsMeta;
try {
customFieldsMeta = JSON.parse(settings.get('Accounts_CustomFields'));
} catch (e) {
logger.debug('Invalid JSON for Custom Fields');
return;
}
if (!getPropertyValue(customFieldsMeta, innerKeys)) {
logger.debug(`user attribute does not exist: ${ userField }`);
return;
}
}
const tmpUserField = getPropertyValue(user, userField);
const tmpLdapField = templateVarHandler(ldapField, ldapUser);
if (tmpLdapField && tmpUserField !== tmpLdapField) {
// creates the object structure instead of just assigning 'tmpLdapField' to
// 'userData[userField]' in order to avoid the "cannot use the part (...)
// to traverse the element" (MongoDB) error that can happen. Do not handle
// arrays.
// TODO: Find a better solution.
const dKeys = userField.split('.');
const lastKey = _.last(dKeys);
_.reduce(dKeys, (obj, currKey) => {
if (currKey === lastKey) {
obj[currKey] = tmpLdapField;
} else {
obj[currKey] = obj[currKey] || {};
}
return obj[currKey];
}, userData);
logger.debug(`user.${ userField } changed to: ${ tmpLdapField }`);
}
}
});
if (emailList.length > 0) {
if (JSON.stringify(user.emails) !== JSON.stringify(emailList)) {
userData.emails = emailList;
}
}
}
const uniqueId = getLdapUserUniqueID(ldapUser);
if (uniqueId && (!user.services || !user.services.ldap || user.services.ldap.id !== uniqueId.value || user.services.ldap.idAttribute !== uniqueId.attribute)) {
userData['services.ldap.id'] = uniqueId.value;
userData['services.ldap.idAttribute'] = uniqueId.attribute;
}
if (user.ldap !== true) {
userData.ldap = true;
}
if (_.size(userData)) {
return userData;
}
}
export function mapLdapGroupsToUserRoles(ldap, ldapUser, user) {
const syncUserRoles = settings.get('LDAP_Sync_User_Data_Groups');
const syncUserRolesAutoRemove = settings.get('LDAP_Sync_User_Data_Groups_AutoRemove');
const syncUserRolesFieldMap = settings.get('LDAP_Sync_User_Data_GroupsMap').trim();
if (!syncUserRoles || !syncUserRolesFieldMap) {
return [];
}
const roles = Roles.find({}, {
fields: {
_updatedAt: 0,
},
}).fetch();
if (!roles) {
return [];
}
let fieldMap;
try {
fieldMap = JSON.parse(syncUserRolesFieldMap);
} catch (err) {
logger.error(`Unexpected error : ${ err.message }`);
return [];
}
if (!fieldMap) {
return [];
}
const userRoles = [];
for (const ldapField in fieldMap) {
if (!fieldMap.hasOwnProperty(ldapField)) {
continue;
}
const userField = fieldMap[ldapField];
const [roleName] = userField.split(/\.(.+)/);
if (!_.find(roles, (el) => el._id === roleName)) {
logger.debug(`User Role doesn't exist: ${ roleName }`);
continue;
}
logger.debug(`User role exists for mapping ${ ldapField } -> ${ roleName }`);
if (isUserInLDAPGroup(ldap, ldapUser, user, ldapField)) {
userRoles.push(roleName);
continue;
}
if (!syncUserRolesAutoRemove) {
continue;
}
const del = Roles.removeUserRoles(user._id, roleName);
if (settings.get('UI_DisplayRoles') && del) {
Notifications.notifyLogged('roles-change', {
type: 'removed',
_id: roleName,
u: {
_id: user._id,
username: user.username,
},
});
}
}
return userRoles;
}
export function createRoomForSync(channel) {
logger.info(`Channel '${ channel }' doesn't exist, creating it.`);
const room = createRoom('c', channel, settings.get('LDAP_Sync_User_Data_Groups_AutoChannels_Admin'), [], false, { customFields: { ldap: true } });
if (!room || !room.rid) {
logger.error(`Unable to auto-create channel '${ channel }' during ldap sync.`);
return;
}
room._id = room.rid;
return room;
}
export function mapLDAPGroupsToChannels(ldap, ldapUser, user) {
const syncUserRoles = settings.get('LDAP_Sync_User_Data_Groups');
const syncUserRolesAutoChannels = settings.get('LDAP_Sync_User_Data_Groups_AutoChannels');
const syncUserRolesEnforceAutoChannels = settings.get('LDAP_Sync_User_Data_Groups_Enforce_AutoChannels');
const syncUserRolesChannelFieldMap = settings.get('LDAP_Sync_User_Data_Groups_AutoChannelsMap').trim();
const userChannels = [];
if (!syncUserRoles || !syncUserRolesAutoChannels || !syncUserRolesChannelFieldMap) {
return [];
}
let fieldMap;
try {
fieldMap = JSON.parse(syncUserRolesChannelFieldMap);
} catch (err) {
logger.error(`Unexpected error : ${ err.message }`);
return [];
}
if (!fieldMap) {
return [];
}
_.map(fieldMap, function(channels, ldapField) {
if (!Array.isArray(channels)) {
channels = [channels];
}
for (const channel of channels) {
let room = Rooms.findOneByName(channel);
if (!room) {
room = createRoomForSync(channel);
}
if (isUserInLDAPGroup(ldap, ldapUser, user, ldapField)) {
userChannels.push(room._id);
} else if (syncUserRolesEnforceAutoChannels) {
const subscription = Subscriptions.findOneByRoomIdAndUserId(room._id, user._id);
if (subscription) {
removeUserFromRoom(room._id, user);
}
}
}
});
return userChannels;
}
export function syncUserData(user, ldapUser, ldap) {
logger.info('Syncing user data');
logger.debug('user', { email: user.email, _id: user._id });
logger.debug('ldapUser', ldapUser.object);
const userData = getDataToSyncUserData(ldapUser, user);
// Returns a list of Rocket.Chat Groups a user should belong
// to if their LDAP group matches the LDAP_Sync_User_Data_GroupsMap
const userRoles = mapLdapGroupsToUserRoles(ldap, ldapUser, user);
// Returns a list of Rocket.Chat Channels a user should belong
// to if their LDAP group matches the LDAP_Sync_User_Data_Groups_AutoChannelsMap
const userChannels = mapLDAPGroupsToChannels(ldap, ldapUser, user);
if (user && user._id && userData) {
logger.debug('setting', JSON.stringify(userData, null, 2));
if (userData.name) {
_setRealName(user._id, userData.name);
delete userData.name;
}
Meteor.users.update(user._id, { $set: userData });
user = Meteor.users.findOne({ _id: user._id });
}
if (settings.get('LDAP_Username_Field') !== '') {
const username = slug(getLdapUsername(ldapUser));
if (user && user._id && username !== user.username) {
logger.info('Syncing user username', user.username, '->', username);
_setUsername(user._id, username);
}
}
if (settings.get('LDAP_Sync_User_Data_Groups') === true) {
for (const roleName of userRoles) {
const add = Roles.addUserRoles(user._id, roleName);
if (settings.get('UI_DisplayRoles') && add) {
Notifications.notifyLogged('roles-change', {
type: 'added',
_id: roleName,
u: {
_id: user._id,
username: user.username,
},
});
}
logger.info('Synced user group', roleName, 'from LDAP for', user.username);
}
}
if (settings.get('LDAP_Sync_User_Data_Groups_AutoChannels') === true) {
for (const userChannel of userChannels) {
addUserToRoom(userChannel, user);
logger.info('Synced user channel', userChannel, 'from LDAP for', user.username);
}
}
if (user && user._id && settings.get('LDAP_Sync_User_Avatar') === true) {
const avatar = ldapUser._raw.thumbnailPhoto || ldapUser._raw.jpegPhoto;
if (avatar) {
logger.info('Syncing user avatar');
const rs = RocketChatFile.bufferToStream(avatar);
const fileStore = FileUpload.getStore('Avatars');
fileStore.deleteByName(user.username);
const file = {
userId: user._id,
type: 'image/jpeg',
};
Meteor.runAsUser(user._id, () => {
fileStore.insert(file, rs, () => {
Meteor.setTimeout(function() {
Users.setAvatarOrigin(user._id, 'ldap');
Notifications.notifyLogged('updateAvatar', { username: user.username });
}, 500);
});
});
}
}
}
export function addLdapUser(ldapUser, username, password, ldap) {
const uniqueId = getLdapUserUniqueID(ldapUser);
const userObject = {};
if (username) {
userObject.username = username;
}
const userData = getDataToSyncUserData(ldapUser, {});
if (userData && userData.emails && userData.emails[0] && userData.emails[0].address) {
if (Array.isArray(userData.emails[0].address)) {
userObject.email = userData.emails[0].address[0];
} else {
userObject.email = userData.emails[0].address;
}
} else if (ldapUser.mail && ldapUser.mail.indexOf('@') > -1) {
userObject.email = ldapUser.mail;
} else if (settings.get('LDAP_Default_Domain') !== '') {
userObject.email = `${ username || uniqueId.value }@${ settings.get('LDAP_Default_Domain') }`;
} else {
const error = new Meteor.Error('LDAP-login-error', 'LDAP Authentication succeeded, there is no email to create an account. Have you tried setting your Default Domain in LDAP Settings?');
logger.error(error);
throw error;
}
logger.debug('New user data', userObject);
if (password) {
userObject.password = password;
}
try {
userObject._id = Accounts.createUser(userObject);
} catch (error) {
logger.error('Error creating user', error);
return error;
}
syncUserData(userObject, ldapUser, ldap);
return {
userId: userObject._id,
};
}
export function importNewUsers(ldap) {
if (settings.get('LDAP_Enable') !== true) {
logger.error('Can\'t run LDAP Import, LDAP is disabled');
return;
}
if (!ldap) {
ldap = new LDAP();
}
if (!ldap.connected) {
ldap.connectSync();
}
let count = 0;
ldap.searchUsersSync('*', Meteor.bindEnvironment((error, ldapUsers, { next, end } = {}) => {
if (error) {
throw error;
}
ldapUsers.forEach((ldapUser) => {
count++;
const uniqueId = getLdapUserUniqueID(ldapUser);
// Look to see if user already exists
const userQuery = {
'services.ldap.id': uniqueId.value,
};
logger.debug('userQuery', userQuery);
let username;
if (settings.get('LDAP_Username_Field') !== '') {
username = slug(getLdapUsername(ldapUser));
}
// Add user if it was not added before
let user = Meteor.users.findOne(userQuery);
if (!user && username && settings.get('LDAP_Merge_Existing_Users') === true) {
const userQuery = {
username,
};
logger.debug('userQuery merge', userQuery);
user = Meteor.users.findOne(userQuery);
if (user) {
syncUserData(user, ldapUser, ldap);
}
}
if (!user) {
addLdapUser(ldapUser, username, undefined, ldap);
}
if (count % 100 === 0) {
logger.info('Import running. Users imported until now:', count);
}
});
if (end) {
logger.info('Import finished. Users imported:', count);
}
next(count);
}));
}
function sync() {
if (settings.get('LDAP_Enable') !== true) {
return;
}
const ldap = new LDAP();
try {
ldap.connectSync();
let users;
if (settings.get('LDAP_Background_Sync_Keep_Existant_Users_Updated') === true) {
users = Users.findLDAPUsers();
}
if (settings.get('LDAP_Background_Sync_Import_New_Users') === true) {
importNewUsers(ldap);
}
if (settings.get('LDAP_Background_Sync_Keep_Existant_Users_Updated') === true) {
users.forEach(function(user) {
let ldapUser;
if (user.services && user.services.ldap && user.services.ldap.id) {
ldapUser = ldap.getUserByIdSync(user.services.ldap.id, user.services.ldap.idAttribute);
} else {
ldapUser = ldap.getUserByUsernameSync(user.username);
}
if (ldapUser) {
syncUserData(user, ldapUser, ldap);
} else {
logger.info('Can\'t sync user', user.username);
}
});
}
} catch (error) {
logger.error(error);
return error;
}
return true;
}
const jobName = 'LDAP_Sync';
const addCronJob = _.debounce(Meteor.bindEnvironment(function addCronJobDebounced() {
if (settings.get('LDAP_Background_Sync') !== true) {
logger.info('Disabling LDAP Background Sync');
if (SyncedCron.nextScheduledAtDate(jobName)) {
SyncedCron.remove(jobName);
}
return;
}
if (settings.get('LDAP_Background_Sync_Interval')) {
logger.info('Enabling LDAP Background Sync');
SyncedCron.add({
name: jobName,
schedule: (parser) => parser.text(settings.get('LDAP_Background_Sync_Interval')),
job() {
sync();
},
});
}
}), 500);
Meteor.startup(() => {
Meteor.defer(() => {
settings.get('LDAP_Background_Sync', addCronJob);
settings.get('LDAP_Background_Sync_Interval', addCronJob);
});
});