[NEW] LDAP User Groups, Roles, and Channel Synchronization (#14278)

pull/15218/head
William Reiske 6 years ago committed by Diego Sampaio
parent 2ca4d696bf
commit 8e84b775dc
  1. 2
      app/ldap/server/loginHandler.js
  2. 32
      app/ldap/server/settings.js
  3. 207
      app/ldap/server/sync.js
  4. 18
      packages/rocketchat-i18n/i18n/en.i18n.json

@ -125,7 +125,7 @@ Accounts.registerLoginHandler('ldap', function(loginRequest) {
logger.info('Logging user');
syncUserData(user, ldapUser);
syncUserData(user, ldapUser, ldap);
if (settings.get('LDAP_Login_Fallback') === true && typeof loginRequest.ldapPass === 'string' && loginRequest.ldapPass.trim() !== '') {
Accounts.setPassword(user._id, loginRequest.ldapPass, { logout: false });

@ -14,6 +14,15 @@ settings.addGroup('LDAP', function() {
enableQuery,
{ _id: 'LDAP_Sync_User_Data', value: true },
];
const syncGroupsQuery = [
enableQuery,
{ _id: 'LDAP_Sync_User_Data_Groups', value: true },
];
const syncGroupsChannelsQuery = [
enableQuery,
{ _id: 'LDAP_Sync_User_Data_Groups', value: true },
{ _id: 'LDAP_Sync_User_Data_Groups_AutoChannels', value: true },
];
const groupFilterQuery = [
enableQuery,
{ _id: 'LDAP_Group_Filter_Enable', value: true },
@ -84,6 +93,29 @@ settings.addGroup('LDAP', function() {
this.add('LDAP_Sync_User_Data', false, { type: 'boolean', enableQuery });
this.add('LDAP_Sync_User_Data_FieldMap', '{"cn":"name", "mail":"email"}', { type: 'string', enableQuery: syncDataQuery });
this.add('LDAP_Sync_User_Data_Groups', false, { type: 'boolean', enableQuery });
this.add('LDAP_Sync_User_Data_Groups_AutoRemove', false, { type: 'boolean', enableQuery: syncGroupsQuery });
this.add('LDAP_Sync_User_Data_Groups_Filter', '(&(cn=#{groupName})(memberUid=#{username}))', { type: 'string', enableQuery: syncGroupsQuery });
this.add('LDAP_Sync_User_Data_Groups_BaseDN', '', { type: 'string', enableQuery: syncGroupsQuery });
this.add('LDAP_Sync_User_Data_GroupsMap', '{\n\t"rocket-admin": "admin",\n\t"tech-support": "support"\n}', {
type: 'code',
multiline: true,
public: false,
code: 'application/json',
enableQuery: syncGroupsQuery,
});
this.add('LDAP_Sync_User_Data_Groups_AutoChannels', false, { type: 'boolean', enableQuery: syncGroupsQuery });
this.add('LDAP_Sync_User_Data_Groups_AutoChannels_Admin', 'rocket.cat', { type: 'string', enableQuery: syncGroupsChannelsQuery });
this.add('LDAP_Sync_User_Data_Groups_AutoChannelsMap', '{\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: syncGroupsChannelsQuery,
});
this.add('LDAP_Sync_User_Data_Groups_Enforce_AutoChannels', false, { type: 'boolean', enableQuery: syncGroupsChannelsQuery });
this.add('LDAP_Sync_User_Avatar', true, { type: 'boolean', enableQuery });
this.add('LDAP_Background_Sync', false, { type: 'boolean', enableQuery });

@ -8,14 +8,40 @@ import LDAP from './ldap';
import { RocketChatFile } from '../../file';
import { settings } from '../../settings';
import { Notifications } from '../../notifications';
import { Users } from '../../models';
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;
@ -175,14 +201,152 @@ export function getDataToSyncUserData(ldapUser, user) {
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.`);
export function syncUserData(user, ldapUser) {
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) {
@ -201,6 +365,30 @@ export function syncUserData(user, ldapUser) {
}
}
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) {
@ -227,7 +415,7 @@ export function syncUserData(user, ldapUser) {
}
}
export function addLdapUser(ldapUser, username, password) {
export function addLdapUser(ldapUser, username, password, ldap) {
const uniqueId = getLdapUserUniqueID(ldapUser);
const userObject = {};
@ -249,7 +437,7 @@ export function addLdapUser(ldapUser, username, password) {
} 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 succeded, there is no email to create an account. Have you tried setting your Default Domain in LDAP Settings?');
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;
}
@ -267,7 +455,7 @@ export function addLdapUser(ldapUser, username, password) {
return error;
}
syncUserData(userObject, ldapUser);
syncUserData(userObject, ldapUser, ldap);
return {
userId: userObject._id,
@ -282,6 +470,9 @@ export function importNewUsers(ldap) {
if (!ldap) {
ldap = new LDAP();
}
if (!ldap.connected) {
ldap.connectSync();
}
@ -319,12 +510,12 @@ export function importNewUsers(ldap) {
user = Meteor.users.findOne(userQuery);
if (user) {
syncUserData(user, ldapUser);
syncUserData(user, ldapUser, ldap);
}
}
if (!user) {
addLdapUser(ldapUser, username);
addLdapUser(ldapUser, username, undefined, ldap);
}
if (count % 100 === 0) {
@ -370,7 +561,7 @@ function sync() {
}
if (ldapUser) {
syncUserData(user, ldapUser);
syncUserData(user, ldapUser, ldap);
} else {
logger.info('Can\'t sync user', user.username);
}

@ -1877,6 +1877,24 @@
"LDAP_Sync_User_Data_Description": "Keep user data in sync with server on **login** or on **background sync** (eg: name, email).",
"LDAP_Sync_User_Data_FieldMap": "User Data Field Map",
"LDAP_Sync_User_Data_FieldMap_Description": "Configure how user account fields (like email) are populated from a record in LDAP (once found). <br/>As an example, `{\"cn\":\"name\", \"mail\":\"email\"}` will choose a person's human readable name from the cn attribute, and their email from the mail attribute. Additionally it is possible to use variables, for example: `{ \"#{givenName} #{sn}\": \"name\", \"mail\": \"email\" }` uses a combination of the user's first name and last name for the rocket chat `name` field.<br/>Available fields in Rocket.Chat: `name`, `email` and `customFields`.",
"LDAP_Sync_User_Data_Groups": "Sync LDAP Groups",
"LDAP_Sync_User_Data_Groups_AutoChannels": "Auto Sync LDAP Groups to Channels",
"LDAP_Sync_User_Data_Groups_AutoChannels_Admin": "Channel Admin",
"LDAP_Sync_User_Data_Groups_AutoChannels_Admin_Description": "When channels are auto-created that do not exist during a sync, this user will automatically become the admin for the channel.",
"LDAP_Sync_User_Data_Groups_AutoChannels_Description": "Enable this feature to automatically add users to a channel based on their LDAP group. If you would like to also remove users from a channel, see the option below about auto removing users.",
"LDAP_Sync_User_Data_Groups_AutoChannelsMap": "LDAP Group Channel Map",
"LDAP_Sync_User_Data_Groups_AutoChannelsMap_Default": "// Enable Auto Sync LDAP Groups to Channels above",
"LDAP_Sync_User_Data_Groups_AutoChannelsMap_Description": "Map LDAP groups to Rocket.Chat channels. <br/>As an example, `{\"employee\":\"general\"}` will add any user in the LDAP group employee, to the general channel.",
"LDAP_Sync_User_Data_Groups_AutoRemove": "Auto Remove User Roles",
"LDAP_Sync_User_Data_Groups_AutoRemove_Description": "**Attention**: Enabling this will automatically remove users from a role if they are not assigned in LDAP! This will only remove roles automatically that are set under the user data group map below.",
"LDAP_Sync_User_Data_Groups_BaseDN": "LDAP Group BaseDN",
"LDAP_Sync_User_Data_Groups_BaseDN_Description": "The LDAP BaseDN used to lookup users.",
"LDAP_Sync_User_Data_Groups_Enforce_AutoChannels": "Auto Remove Users from Channels",
"LDAP_Sync_User_Data_Groups_Enforce_AutoChannels_Description": "**Attention**: Enabling this will remove any users in a channel that do not have the coorosponding LDAP group! Only enable this if you know what you're doing.",
"LDAP_Sync_User_Data_Groups_Filter": "User Group Filter",
"LDAP_Sync_User_Data_Groups_Filter_Description": "The LDAP search filter used to check if a user is in a group.",
"LDAP_Sync_User_Data_GroupsMap": "User Data Group Map",
"LDAP_Sync_User_Data_GroupsMap_Description": "Map LDAP groups to Rocket.Chat user roles <br/>As an example, `{\"rocket-admin\":\"admin\", \"tech-support\":\"support\"}` will map the rocket-admin LDAP group to Rocket's \"admin\" role.",
"LDAP_Test_Connection": "Test Connection",
"LDAP_Timeout": "Timeout (ms)",
"LDAP_Timeout_Description": "How many mileseconds wait for a search result before return an error",

Loading…
Cancel
Save