Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 36 commits into from
Aug 21, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
543a41d
Init commit of LDAP user role / group synchronization
wreiske Apr 28, 2019
f6dddbf
Merge branch 'develop' of https://github.com/RocketChat/Rocket.Chat i…
wreiske Apr 28, 2019
58ffc4a
i18n whoopsie
wreiske Apr 28, 2019
6feaed7
Removed a duplicated line.. whoopsie
wreiske Apr 28, 2019
ceb0fe1
Merge branch 'develop' into ldap-admin-groups
wreiske Apr 28, 2019
770d536
Added auto channel join / leave by LDAP Sync
wreiske Apr 29, 2019
afb576a
Merge branch 'develop' into ldap-admin-groups
wreiske Apr 29, 2019
7c82314
i18n whoopsie
wreiske Apr 29, 2019
a19f055
Merge branch 'develop' into ldap-admin-groups
wreiske May 2, 2019
deac02c
Only run remove code if the user is actually in the room
wreiske May 2, 2019
f9c2061
Goes with last commit...
wreiske May 2, 2019
ce5fc13
Merge branch 'develop' into ldap-admin-groups
wreiske May 9, 2019
9857415
Merge branch 'develop' into ldap-admin-groups
wreiske May 15, 2019
00bcb18
Merge branch 'develop' into ldap-admin-groups
wreiske May 16, 2019
5d5be5b
Merge branch 'develop' into ldap-admin-groups
wreiske Jun 3, 2019
b7165a0
Fixed build + added auto channel creation
wreiske Jun 3, 2019
2e04d2d
Merge branch 'develop' into ldap-admin-groups
wreiske Jun 6, 2019
d488b1c
Merge branch 'develop' into ldap-admin-groups
wreiske Jun 13, 2019
6ed1044
Merge branch 'develop' into ldap-admin-groups
wreiske Jun 18, 2019
9be9683
Merge branch 'develop' into ldap-admin-groups
wreiske Jun 24, 2019
68d9015
Merge branch 'develop' into ldap-admin-groups
wreiske Jun 27, 2019
9b677da
Merge branch 'develop' into ldap-admin-groups
wreiske Jul 4, 2019
fb8b5f5
Merge branch 'develop' into ldap-admin-groups
engelgabriel Aug 1, 2019
ecc62bb
Merge branch 'develop' into ldap-admin-groups
wreiske Aug 16, 2019
fd17cf5
Merge branch 'develop' into ldap-admin-groups
wreiske Aug 17, 2019
bb3e0e8
Merge branch 'develop' into ldap-admin-groups
wreiske Aug 18, 2019
f537341
fixed query used on new setting
Hudell Aug 20, 2019
4e22be2
Fixed notification type
Hudell Aug 20, 2019
87f5277
Fixed indentation and other styling rules
Hudell Aug 20, 2019
3f75c50
Removed unnecessary underscore calls
Hudell Aug 20, 2019
feb4603
use a local Ldap instance for all connections
Hudell Aug 20, 2019
c54aec1
Use try...catch to handle unexpected exceptions
Hudell Aug 20, 2019
f1b60b1
Merge branch 'develop' into ldap-admin-groups
geekgonecrazy Aug 20, 2019
ebbc33f
Code formatting
Hudell Aug 20, 2019
9e060d7
Merge branch 'ldap-admin-groups' of github.com:wreiske/Rocket.Chat in…
Hudell Aug 20, 2019
23b043c
Reuse ldap connection
Hudell Aug 21, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/ldap/server/loginHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
32 changes: 32 additions & 0 deletions app/ldap/server/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down Expand Up @@ -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 });
Expand Down
207 changes: 199 additions & 8 deletions app/ldap/server/sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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 = {};
Expand All @@ -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;
}
Expand All @@ -267,7 +455,7 @@ export function addLdapUser(ldapUser, username, password) {
return error;
}

syncUserData(userObject, ldapUser);
syncUserData(userObject, ldapUser, ldap);

return {
userId: userObject._id,
Expand All @@ -282,6 +470,9 @@ export function importNewUsers(ldap) {

if (!ldap) {
ldap = new LDAP();
Hudell marked this conversation as resolved.
Show resolved Hide resolved
}

if (!ldap.connected) {
ldap.connectSync();
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -370,7 +561,7 @@ function sync() {
}

if (ldapUser) {
syncUserData(user, ldapUser);
syncUserData(user, ldapUser, ldap);
} else {
logger.info('Can\'t sync user', user.username);
}
Expand Down
18 changes: 18 additions & 0 deletions packages/rocketchat-i18n/i18n/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down