Closes #2262; Implement LDAP user sync

pull/2282/head
Rodrigo Nascimento 9 years ago
parent 2c8a92b289
commit 25e9a88827
  1. 5
      i18n/en.i18n.json
  2. 2
      packages/rocketchat-ldap/package.js
  3. 90
      packages/rocketchat-ldap/server/ldap.js
  4. 116
      packages/rocketchat-ldap/server/loginHandler.js
  5. 1
      packages/rocketchat-ldap/server/settings.coffee
  6. 156
      packages/rocketchat-ldap/server/sync.js
  7. 27
      packages/rocketchat-ldap/server/syncUsers.js
  8. 6
      packages/rocketchat-lib/server/models/Users.coffee

@ -209,8 +209,8 @@
"Hide_room" : "Hide room",
"Hide_Room_Warning" : "Are you sure you want to hide the room \"%s\"?",
"Highlights": "Highlights",
"Highlights_List": "Highlight words",
"Highlights_How_To": "To be notified when someone mentions a word or phrase, add it here. You can separate words or phrases with commas. Highlight Words are not case sensitive.",
"Highlights_List": "Highlight words",
"History" : "History",
"hours" : "hours",
"Incorrect_Password" : "Incorrect Password",
@ -302,6 +302,7 @@
"LDAP_Sync_User_Data_Description" : "Keep user data in sync with server on login (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.<br/> Available fields include `name`, and `email`.",
"LDAP_Sync_Users" : "Sync Users",
"LDAP_Test_Connection" : "Test Connection",
"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.<br/>Default value is `objectGUID,ibm-entryUUID,GUID,dominoUNID,nsuniqueId,uidNumber`",
@ -569,6 +570,8 @@
"strike" : "strike",
"Submit" : "Submit",
"Success" : "Success",
"Sync_success" : "Sync success",
"Sync_Users" : "Sync Users",
"Test_Connection" : "Test Connection",
"Test_Desktop_Notifications" : "Test Desktop Notifications",
"The_application_name_is_required" : "Th _application name is required",

@ -25,9 +25,11 @@ Package.onUse(function(api) {
api.addFiles('client/loginHelper.js', 'client');
api.addFiles('server/ldap.js', 'server');
api.addFiles('server/sync.js', 'server');
api.addFiles('server/loginHandler.js', 'server');
api.addFiles('server/settings.coffee', 'server');
api.addFiles('server/testConnection.js', 'server');
api.addFiles('server/syncUsers.js', 'server');
api.export('LDAP', 'server');
});

@ -201,11 +201,16 @@ LDAP = class LDAP {
bindIfNecessary() {
const self = this;
if (self.domainBinded === true) {
return;
}
const domain_search = self.getDomainBindSearch();
if (domain_search.domain_search_user !== '' && domain_search.domain_search_password !== '') {
logger.bind_info('Binding admin user', domain_search.domain_search_user);
self.bindSync(domain_search.domain_search_user, domain_search.domain_search_password);
self.domainBinded = true;
}
}
@ -228,6 +233,85 @@ LDAP = class LDAP {
return self.searchAllSync(self.options.domain_base, searchOptions);
}
getUserByIdSync(id, attribute) {
const self = this;
self.bindIfNecessary();
const domain_search = self.getDomainBindSearch();
let Unique_Identifier_Field = RocketChat.settings.get('LDAP_Unique_Identifier_Field').split(',');
let filter;
if (attribute) {
filter = new self.ldapjs.filters.EqualityFilter({
attribute: attribute,
value: new Buffer(id, 'hex')
});
} else {
const filters = [];
Unique_Identifier_Field.forEach(function(item) {
filters.push(new self.ldapjs.filters.EqualityFilter({
attribute: item,
value: new Buffer(id, 'hex')
}));
});
filter = new self.ldapjs.filters.OrFilter({filters: filters});
}
const searchOptions = {
filter: filter,
scope: 'sub'
};
logger.search_info('Searching by id', id);
logger.search_debug('search filter', searchOptions.filter.toString());
logger.search_debug('domain_base', self.options.domain_base);
const result = self.searchAllSync(self.options.domain_base, searchOptions);
if (!Array.isArray(result) || result.length === 0) {
return;
}
if (result.length > 1) {
logger.search_error('Search by id', id, 'returned', result.length, 'records');
}
return result[0];
}
getUserByUsernameSync(username) {
const self = this;
self.bindIfNecessary();
const domain_search = self.getDomainBindSearch();
const searchOptions = {
filter: domain_search.filter.replace(/#{username}/g, username),
scope: 'sub'
};
logger.search_info('Searching user', username);
logger.search_debug('searchOptions', searchOptions);
logger.search_debug('domain_base', self.options.domain_base);
const result = self.searchAllSync(self.options.domain_base, searchOptions);
if (!Array.isArray(result) || result.length === 0) {
return;
}
if (result.length > 1) {
logger.search_error('Search by id', id, 'returned', result.length, 'records');
}
return result[0];
}
searchAllAsync(domain_base, options, callback) {
const self = this;
@ -244,15 +328,17 @@ LDAP = class LDAP {
return;
});
let entries = [];
const entries = [];
const jsonEntries = [];
res.on('searchEntry', function(entry) {
entries.push(entry);
jsonEntries.push(entry.json);
});
res.on('end', function(result) {
logger.search_info('Search result count', entries.length);
logger.search_debug('Search result', entries);
logger.search_debug('Search result', JSON.stringify(jsonEntries, null, 2));
callback(null, entries);
});
});

@ -8,6 +8,7 @@ var slug = function (text) {
return text.replace(/[^0-9a-z-_.]/g, '');
};
function fallbackDefaultAccountSystem(bind, username, password) {
if (typeof username === 'string')
if (username.indexOf('@') === -1)
@ -28,88 +29,6 @@ function fallbackDefaultAccountSystem(bind, username, password) {
return Accounts._runLoginHandlers(bind, loginRequest);
}
function getDataToSyncUserData(ldapUser) {
const syncUserData = RocketChat.settings.get('LDAP_Sync_User_Data');
const syncUserDataFieldMap = RocketChat.settings.get('LDAP_Sync_User_Data_FieldMap').trim();
if (syncUserData && syncUserDataFieldMap) {
const fieldMap = JSON.parse(syncUserDataFieldMap);
let userData = {};
let emailList = [];
_.map(fieldMap, function(userField, ldapField) {
if (!ldapUser.object.hasOwnProperty(ldapField)) {
return;
}
switch (userField) {
case 'email':
if (_.isObject(ldapUser.object[ldapField] === 'object')) {
_.map(ldapUser.object[ldapField], function (item) {
emailList.push({ address: item, verified: true });
});
} else {
emailList.push({ address: ldapUser.object[ldapField], verified: true });
}
break;
case 'name':
userData.name = ldapUser.object[ldapField];
break;
}
});
if (emailList.length > 0) {
userData.emails = emailList;
}
if (_.size(userData)) {
return userData;
}
}
}
function syncUserData(user, ldapUser) {
logger.info('Syncing user data');
logger.debug('user', user);
logger.debug('ldapUser', ldapUser);
const userData = getDataToSyncUserData(ldapUser);
if (user && user._id && userData) {
Meteor.users.update(user._id, { $set: userData });
}
if (user && user._id) {
const avatar = ldapUser.raw.thumbnailPhoto || ldapUser.raw.jpegPhoto;
if (avatar) {
logger.info('Syncing user avatar');
const rs = RocketChatFile.bufferToStream(avatar);
RocketChatFileAvatarInstance.deleteFile(encodeURIComponent(`${user.username}.jpg`));
const ws = RocketChatFileAvatarInstance.createWriteStream(encodeURIComponent(`${user.username}.jpg`), 'image/jpeg');
ws.on('end', Meteor.bindEnvironment(function() {
Meteor.setTimeout(function() {
RocketChat.models.Users.setAvatarOrigin(user._id, 'ldap');
RocketChat.Notifications.notifyAll('updateAvatar', {username: user.username});
}, 500);
}));
rs.pipe(ws);
}
}
}
function getLdapUserUniqueID(ldapUser, fallback) {
let Unique_Identifier_Field = RocketChat.settings.get('LDAP_Unique_Identifier_Field');
if (Unique_Identifier_Field !== '') {
Unique_Identifier_Field = Unique_Identifier_Field.split(',').find((field) => {
return !_.isEmpty(ldapUser.object[field]);
});
if (Unique_Identifier_Field) {
Unique_Identifier_Field = ldapUser.raw[Unique_Identifier_Field].toString('hex');
}
return Unique_Identifier_Field || fallback;
}
}
Accounts.registerLoginHandler("ldap", function(loginRequest) {
const self = this;
@ -162,21 +81,25 @@ Accounts.registerLoginHandler("ldap", function(loginRequest) {
// Look to see if user already exists
let userQuery;
let Unique_Identifier_Field = getLdapUserUniqueID(ldapUser, username);
if (Unique_Identifier_Field) {
userQuery = {
'services.ldap.id': Unique_Identifier_Field
};
} else {
let Unique_Identifier_Field = getLdapUserUniqueID(ldapUser, 'username', username);
userQuery = {
'services.ldap.id': Unique_Identifier_Field.value
};
logger.info('Querying user');
logger.debug('userQuery', userQuery);
let user = Meteor.users.findOne(userQuery);
if (!user) {
userQuery = {
username: username
};
}
logger.info('Querying user');
logger.debug('userQuery', userQuery);
logger.debug('userQuery', userQuery);
const user = Meteor.users.findOne(userQuery);
user = Meteor.users.findOne(userQuery);
}
// Login user if they exist
if (user) {
@ -195,7 +118,7 @@ Accounts.registerLoginHandler("ldap", function(loginRequest) {
}
});
syncUserData(user, ldapUser, loginRequest.ldapPass);
syncUserData(user, ldapUser);
Accounts.setPassword(user._id, loginRequest.ldapPass, {logout: false});
return {
userId: user._id,
@ -210,7 +133,7 @@ Accounts.registerLoginHandler("ldap", function(loginRequest) {
password: loginRequest.ldapPass
};
let userData = getDataToSyncUserData(ldapUser);
let userData = getDataToSyncUserData(ldapUser, {});
if (userData && userData.emails) {
userObject.email = userData.emails[0].address;
@ -224,14 +147,15 @@ Accounts.registerLoginHandler("ldap", function(loginRequest) {
userObject._id = Accounts.createUser(userObject);
syncUserData(userObject, ldapUser, loginRequest.ldapPass);
syncUserData(userObject, ldapUser);
let ldapUserService = {
ldap: true
};
if (Unique_Identifier_Field) {
ldapUserService['services.ldap.id'] = Unique_Identifier_Field;
ldapUserService['services.ldap.idAttribute'] = Unique_Identifier_Field.attribute;
ldapUserService['services.ldap.id'] = Unique_Identifier_Field.value;
}
Meteor.users.update(userObject._id, {

@ -39,3 +39,4 @@ Meteor.startup ->
@add 'LDAP_Sync_User_Data_FieldMap', '{"cn":"name", "mail":"email"}', { type: 'string', enableQuery: syncDataQuery }
@add 'LDAP_Default_Domain', '', { type: 'string' , enableQuery: enableQuery }
@add 'LDAP_Test_Connection', 'ldap_test_connection', { type: 'action', actionText: 'Test_Connection' }
@add 'LDAP_Sync_Users', 'ldap_sync_users', { type: 'action', actionText: 'Sync_Users' }

@ -0,0 +1,156 @@
const logger = new Logger('LDAPSync', {});
getLdapUserUniqueID = function getLdapUserUniqueID(ldapUser, fallbackAttribute, fallbackValue) {
let Unique_Identifier_Field = RocketChat.settings.get('LDAP_Unique_Identifier_Field');
if (Unique_Identifier_Field !== '') {
Unique_Identifier_Field = Unique_Identifier_Field.split(',').find((field) => {
return !_.isEmpty(ldapUser.object[field]);
});
if (Unique_Identifier_Field) {
Unique_Identifier_Field = {
attribute: Unique_Identifier_Field,
value: ldapUser.raw[Unique_Identifier_Field].toString('hex')
};
}
return Unique_Identifier_Field || {attribute: fallbackAttribute, value: fallbackValue};
}
};
getDataToSyncUserData = function getDataToSyncUserData(ldapUser, user) {
const syncUserData = RocketChat.settings.get('LDAP_Sync_User_Data');
const syncUserDataFieldMap = RocketChat.settings.get('LDAP_Sync_User_Data_FieldMap').trim();
if (syncUserData && syncUserDataFieldMap) {
const fieldMap = JSON.parse(syncUserDataFieldMap);
let userData = {};
let emailList = [];
_.map(fieldMap, function(userField, ldapField) {
if (!ldapUser.object.hasOwnProperty(ldapField)) {
return;
}
switch (userField) {
case 'email':
if (_.isObject(ldapUser.object[ldapField] === 'object')) {
_.map(ldapUser.object[ldapField], function (item) {
emailList.push({ address: item, verified: true });
});
} else {
emailList.push({ address: ldapUser.object[ldapField], verified: true });
}
break;
case 'name':
if (user.name !== ldapUser.object[ldapField]) {
userData.name = ldapUser.object[ldapField];
}
break;
}
});
if (emailList.length > 0) {
if (JSON.stringify(user.emails) !== JSON.stringify(emailList)) {
userData.emails = emailList;
}
}
const uniqueId = getLdapUserUniqueID(ldapUser, 'username', user.username);
if (!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 (_.size(userData)) {
return userData;
}
}
};
syncUserData = function syncUserData(user, ldapUser) {
logger.info('Syncing user data');
logger.debug('user', user);
logger.debug('ldapUser', ldapUser);
const userData = getDataToSyncUserData(ldapUser, user);
if (user && user._id && userData) {
Meteor.users.update(user._id, { $set: userData });
logger.debug('setting', JSON.stringify(userData, null, 2));
}
if (user && user._id) {
const avatar = ldapUser.raw.thumbnailPhoto || ldapUser.raw.jpegPhoto;
if (avatar) {
logger.info('Syncing user avatar');
const rs = RocketChatFile.bufferToStream(avatar);
RocketChatFileAvatarInstance.deleteFile(encodeURIComponent(`${user.username}.jpg`));
const ws = RocketChatFileAvatarInstance.createWriteStream(encodeURIComponent(`${user.username}.jpg`), 'image/jpeg');
ws.on('end', Meteor.bindEnvironment(function() {
Meteor.setTimeout(function() {
RocketChat.models.Users.setAvatarOrigin(user._id, 'ldap');
RocketChat.Notifications.notifyAll('updateAvatar', {username: user.username});
}, 500);
}));
rs.pipe(ws);
}
}
};
sync = function sync() {
if (RocketChat.settings.get('LDAP_Enable') !== true) {
return;
}
const ldap = new LDAP();
try {
ldap.connectSync();
users = RocketChat.models.Users.findLDAPUsers();
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);
} else {
logger.info('Can\'t sync user', user.username);
}
});
} catch(error) {
logger.error(error);
return error;
}
ldap.disconnect();
return true;
};
let interval;
let timer;
RocketChat.settings.get('LDAP_Sync_User_Data', function(key, value) {
Meteor.clearInterval(interval);
Meteor.clearTimeout(timeout);
if (value === true) {
logger.info('Enabling LDAP user sync');
interval = Meteor.setInterval(sync, 1000 * 60 * 60);
timeout = Meteor.setTimeout(function() {
sync();
}, 1000 * 30);
} else {
logger.info('Disabling LDAP user sync');
}
});

@ -0,0 +1,27 @@
Meteor.methods({
ldap_sync_users: function() {
user = Meteor.user();
if (!user) {
throw new Meteor.Error('unauthorized', '[methods] ldap_sync_users -> Unauthorized');
}
if (!RocketChat.authz.hasRole(user._id, 'admin')) {
throw new Meteor.Error('unauthorized', '[methods] ldap_sync_users -> Unauthorized');
}
if (RocketChat.settings.get('LDAP_Enable') !== true) {
throw new Meteor.Error('LDAP_disabled');
}
result = sync();
if (result === true) {
return {
message: "Sync_success",
params: []
};
}
throw result;
}
});

@ -124,6 +124,12 @@ RocketChat.models.Users = new class extends RocketChat.models._Base
return @find query, options
findLDAPUsers: (options) ->
query =
ldap: true
return @find query, options
getLastLogin: (options = {}) ->
query = { lastLogin: { $exists: 1 } }
options.sort = { lastLogin: -1 }

Loading…
Cancel
Save