Include to Wekan packages directory contents, so that meteor command would build all directly.

This also simplifies build scripts.

Thanks to xet7 !
This commit is contained in:
Lauri Ojansivu 2019-04-20 15:18:33 +03:00
parent 6117097a93
commit 73e265d8fd
354 changed files with 36977 additions and 106 deletions

View file

@ -0,0 +1 @@
import './loginHandler';

View file

@ -0,0 +1,555 @@
import ldapjs from 'ldapjs';
import util from 'util';
import Bunyan from 'bunyan';
import { log_debug, log_info, log_warn, log_error } from './logger';
export default class LDAP {
constructor(){
this.ldapjs = ldapjs;
this.connected = false;
this.options = {
host: this.constructor.settings_get('LDAP_HOST'),
port: this.constructor.settings_get('LDAP_PORT'),
Reconnect: this.constructor.settings_get('LDAP_RECONNECT'),
timeout: this.constructor.settings_get('LDAP_TIMEOUT'),
connect_timeout: this.constructor.settings_get('LDAP_CONNECT_TIMEOUT'),
idle_timeout: this.constructor.settings_get('LDAP_IDLE_TIMEOUT'),
encryption: this.constructor.settings_get('LDAP_ENCRYPTION'),
ca_cert: this.constructor.settings_get('LDAP_CA_CERT'),
reject_unauthorized: this.constructor.settings_get('LDAP_REJECT_UNAUTHORIZED') || false,
Authentication: this.constructor.settings_get('LDAP_AUTHENTIFICATION'),
Authentication_UserDN: this.constructor.settings_get('LDAP_AUTHENTIFICATION_USERDN'),
Authentication_Password: this.constructor.settings_get('LDAP_AUTHENTIFICATION_PASSWORD'),
Authentication_Fallback: this.constructor.settings_get('LDAP_LOGIN_FALLBACK'),
BaseDN: this.constructor.settings_get('LDAP_BASEDN'),
Internal_Log_Level: this.constructor.settings_get('INTERNAL_LOG_LEVEL'),
User_Search_Filter: this.constructor.settings_get('LDAP_USER_SEARCH_FILTER'),
User_Search_Scope: this.constructor.settings_get('LDAP_USER_SEARCH_SCOPE'),
User_Search_Field: this.constructor.settings_get('LDAP_USER_SEARCH_FIELD'),
Search_Page_Size: this.constructor.settings_get('LDAP_SEARCH_PAGE_SIZE'),
Search_Size_Limit: this.constructor.settings_get('LDAP_SEARCH_SIZE_LIMIT'),
group_filter_enabled: this.constructor.settings_get('LDAP_GROUP_FILTER_ENABLE'),
group_filter_object_class: this.constructor.settings_get('LDAP_GROUP_FILTER_OBJECTCLASS'),
group_filter_group_id_attribute: this.constructor.settings_get('LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE'),
group_filter_group_member_attribute: this.constructor.settings_get('LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE'),
group_filter_group_member_format: this.constructor.settings_get('LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT'),
group_filter_group_name: this.constructor.settings_get('LDAP_GROUP_FILTER_GROUP_NAME'),
};
}
static settings_get(name, ...args) {
let value = process.env[name];
if (value !== undefined) {
if (value === 'true' || value === 'false') {
value = JSON.parse(value);
} else if (value !== '' && !isNaN(value)) {
value = Number(value);
}
return value;
} else {
log_warn(`Lookup for unset variable: ${name}`);
}
}
connectSync(...args) {
if (!this._connectSync) {
this._connectSync = Meteor.wrapAsync(this.connectAsync, this);
}
return this._connectSync(...args);
}
searchAllSync(...args) {
if (!this._searchAllSync) {
this._searchAllSync = Meteor.wrapAsync(this.searchAllAsync, this);
}
return this._searchAllSync(...args);
}
connectAsync(callback) {
log_info('Init setup');
let replied = false;
const connectionOptions = {
url: `${ this.options.host }:${ this.options.port }`,
timeout: this.options.timeout,
connectTimeout: this.options.connect_timeout,
idleTimeout: this.options.idle_timeout,
reconnect: this.options.Reconnect,
};
if (this.options.Internal_Log_Level !== 'disabled') {
connectionOptions.log = new Bunyan({
name: 'ldapjs',
component: 'client',
stream: process.stderr,
level: this.options.Internal_Log_Level,
});
}
const tlsOptions = {
rejectUnauthorized: this.options.reject_unauthorized,
};
if (this.options.ca_cert && this.options.ca_cert !== '') {
// Split CA cert into array of strings
const chainLines = this.constructor.settings_get('LDAP_CA_CERT').split('\n');
let cert = [];
const ca = [];
chainLines.forEach((line) => {
cert.push(line);
if (line.match(/-END CERTIFICATE-/)) {
ca.push(cert.join('\n'));
cert = [];
}
});
tlsOptions.ca = ca;
}
if (this.options.encryption === 'ssl') {
connectionOptions.url = `ldaps://${ connectionOptions.url }`;
connectionOptions.tlsOptions = tlsOptions;
} else {
connectionOptions.url = `ldap://${ connectionOptions.url }`;
}
log_info('Connecting', connectionOptions.url);
log_debug(`connectionOptions${ util.inspect(connectionOptions)}`);
this.client = ldapjs.createClient(connectionOptions);
this.bindSync = Meteor.wrapAsync(this.client.bind, this.client);
this.client.on('error', (error) => {
log_error('connection', error);
if (replied === false) {
replied = true;
callback(error, null);
}
});
this.client.on('idle', () => {
log_info('Idle');
this.disconnect();
});
this.client.on('close', () => {
log_info('Closed');
});
if (this.options.encryption === 'tls') {
// Set host parameter for tls.connect which is used by ldapjs starttls. This shouldn't be needed in newer nodejs versions (e.g v5.6.0).
// https://github.com/RocketChat/Rocket.Chat/issues/2035
// https://github.com/mcavage/node-ldapjs/issues/349
tlsOptions.host = this.options.host;
log_info('Starting TLS');
log_debug('tlsOptions', tlsOptions);
this.client.starttls(tlsOptions, null, (error, response) => {
if (error) {
log_error('TLS connection', error);
if (replied === false) {
replied = true;
callback(error, null);
}
return;
}
log_info('TLS connected');
this.connected = true;
if (replied === false) {
replied = true;
callback(null, response);
}
});
} else {
this.client.on('connect', (response) => {
log_info('LDAP connected');
this.connected = true;
if (replied === false) {
replied = true;
callback(null, response);
}
});
}
setTimeout(() => {
if (replied === false) {
log_error('connection time out', connectionOptions.connectTimeout);
replied = true;
callback(new Error('Timeout'));
}
}, connectionOptions.connectTimeout);
}
getUserFilter(username) {
const filter = [];
if (this.options.User_Search_Filter !== '') {
if (this.options.User_Search_Filter[0] === '(') {
filter.push(`${ this.options.User_Search_Filter }`);
} else {
filter.push(`(${ this.options.User_Search_Filter })`);
}
}
const usernameFilter = this.options.User_Search_Field.split(',').map((item) => `(${ item }=${ username })`);
if (usernameFilter.length === 0) {
log_error('LDAP_LDAP_User_Search_Field not defined');
} else if (usernameFilter.length === 1) {
filter.push(`${ usernameFilter[0] }`);
} else {
filter.push(`(|${ usernameFilter.join('') })`);
}
return `(&${ filter.join('') })`;
}
bindIfNecessary() {
if (this.domainBinded === true) {
return;
}
if (this.options.Authentication !== true) {
return;
}
log_info('Binding UserDN', this.options.Authentication_UserDN);
this.bindSync(this.options.Authentication_UserDN, this.options.Authentication_Password);
this.domainBinded = true;
}
searchUsersSync(username, page) {
this.bindIfNecessary();
const searchOptions = {
filter: this.getUserFilter(username),
scope: this.options.User_Search_Scope || 'sub',
sizeLimit: this.options.Search_Size_Limit,
};
if (this.options.Search_Page_Size > 0) {
searchOptions.paged = {
pageSize: this.options.Search_Page_Size,
pagePause: !!page,
};
}
log_info('Searching user', username);
log_debug('searchOptions', searchOptions);
log_debug('BaseDN', this.options.BaseDN);
if (page) {
return this.searchAllPaged(this.options.BaseDN, searchOptions, page);
}
return this.searchAllSync(this.options.BaseDN, searchOptions);
}
getUserByIdSync(id, attribute) {
this.bindIfNecessary();
const Unique_Identifier_Field = this.constructor.settings_get('LDAP_UNIQUE_IDENTIFIER_FIELD').split(',');
let filter;
if (attribute) {
filter = new this.ldapjs.filters.EqualityFilter({
attribute,
value: new Buffer(id, 'hex'),
});
} else {
const filters = [];
Unique_Identifier_Field.forEach((item) => {
filters.push(new this.ldapjs.filters.EqualityFilter({
attribute: item,
value: new Buffer(id, 'hex'),
}));
});
filter = new this.ldapjs.filters.OrFilter({filters});
}
const searchOptions = {
filter,
scope: 'sub',
};
log_info('Searching by id', id);
log_debug('search filter', searchOptions.filter.toString());
log_debug('BaseDN', this.options.BaseDN);
const result = this.searchAllSync(this.options.BaseDN, searchOptions);
if (!Array.isArray(result) || result.length === 0) {
return;
}
if (result.length > 1) {
log_error('Search by id', id, 'returned', result.length, 'records');
}
return result[0];
}
getUserByUsernameSync(username) {
this.bindIfNecessary();
const searchOptions = {
filter: this.getUserFilter(username),
scope: this.options.User_Search_Scope || 'sub',
};
log_info('Searching user', username);
log_debug('searchOptions', searchOptions);
log_debug('BaseDN', this.options.BaseDN);
const result = this.searchAllSync(this.options.BaseDN, searchOptions);
if (!Array.isArray(result) || result.length === 0) {
return;
}
if (result.length > 1) {
log_error('Search by username', username, 'returned', result.length, 'records');
}
return result[0];
}
getUserGroups(username, ldapUser){
if (!this.options.group_filter_enabled) {
return true;
}
const filter = ['(&'];
if (this.options.group_filter_object_class !== '') {
filter.push(`(objectclass=${ this.options.group_filter_object_class })`);
}
if (this.options.group_filter_group_member_attribute !== '') {
const format_value = ldapUser[this.options.group_filter_group_member_format];
if( format_value ) {
filter.push(`(${ this.options.group_filter_group_member_attribute }=${ format_value })`);
}
}
filter.push(')');
const searchOptions = {
filter: filter.join('').replace(/#{username}/g, username),
scope: 'sub',
};
log_debug('Group list filter LDAP:', searchOptions.filter);
const result = this.searchAllSync(this.options.BaseDN, searchOptions);
if (!Array.isArray(result) || result.length === 0) {
return [];
}
const grp_identifier = this.options.group_filter_group_id_attribute || 'cn';
const groups = [];
result.map((item) => {
groups.push( item[ grp_identifier ] );
});
log_debug(`Groups: ${ groups.join(', ')}`);
return groups;
}
isUserInGroup(username, ldapUser) {
if (!this.options.group_filter_enabled) {
return true;
}
const grps = this.getUserGroups(username, ldapUser);
const filter = ['(&'];
if (this.options.group_filter_object_class !== '') {
filter.push(`(objectclass=${ this.options.group_filter_object_class })`);
}
if (this.options.group_filter_group_member_attribute !== '') {
const format_value = ldapUser[this.options.group_filter_group_member_format];
if( format_value ) {
filter.push(`(${ this.options.group_filter_group_member_attribute }=${ format_value })`);
}
}
if (this.options.group_filter_group_id_attribute !== '') {
filter.push(`(${ this.options.group_filter_group_id_attribute }=${ this.options.group_filter_group_name })`);
}
filter.push(')');
const searchOptions = {
filter: filter.join('').replace(/#{username}/g, username),
scope: 'sub',
};
log_debug('Group filter LDAP:', searchOptions.filter);
const result = this.searchAllSync(this.options.BaseDN, searchOptions);
if (!Array.isArray(result) || result.length === 0) {
return false;
}
return true;
}
extractLdapEntryData(entry) {
const values = {
_raw: entry.raw,
};
Object.keys(values._raw).forEach((key) => {
const value = values._raw[key];
if (!['thumbnailPhoto', 'jpegPhoto'].includes(key)) {
if (value instanceof Buffer) {
values[key] = value.toString();
} else {
values[key] = value;
}
}
});
return values;
}
searchAllPaged(BaseDN, options, page) {
this.bindIfNecessary();
const processPage = ({entries, title, end, next}) => {
log_info(title);
// Force LDAP idle to wait the record processing
this.client._updateIdle(true);
page(null, entries, {end, next: () => {
// Reset idle timer
this.client._updateIdle();
next && next();
}});
};
this.client.search(BaseDN, options, (error, res) => {
if (error) {
log_error(error);
page(error);
return;
}
res.on('error', (error) => {
log_error(error);
page(error);
return;
});
let entries = [];
const internalPageSize = options.paged && options.paged.pageSize > 0 ? options.paged.pageSize * 2 : 500;
res.on('searchEntry', (entry) => {
entries.push(this.extractLdapEntryData(entry));
if (entries.length >= internalPageSize) {
processPage({
entries,
title: 'Internal Page',
end: false,
});
entries = [];
}
});
res.on('page', (result, next) => {
if (!next) {
this.client._updateIdle(true);
processPage({
entries,
title: 'Final Page',
end: true,
});
} else if (entries.length) {
log_info('Page');
processPage({
entries,
title: 'Page',
end: false,
next,
});
entries = [];
}
});
res.on('end', () => {
if (entries.length) {
processPage({
entries,
title: 'Final Page',
end: true,
});
entries = [];
}
});
});
}
searchAllAsync(BaseDN, options, callback) {
this.bindIfNecessary();
this.client.search(BaseDN, options, (error, res) => {
if (error) {
log_error(error);
callback(error);
return;
}
res.on('error', (error) => {
log_error(error);
callback(error);
return;
});
const entries = [];
res.on('searchEntry', (entry) => {
entries.push(this.extractLdapEntryData(entry));
});
res.on('end', () => {
log_info('Search result count', entries.length);
callback(null, entries);
});
});
}
authSync(dn, password) {
log_info('Authenticating', dn);
try {
if (password === '') {
throw new Error('Password is not provided');
}
this.bindSync(dn, password);
log_info('Authenticated', dn);
return true;
} catch (error) {
log_info('Not authenticated', dn);
log_debug('error', error);
return false;
}
}
disconnect() {
this.connected = false;
this.domainBinded = false;
log_info('Disconecting');
this.client.unbind();
}
}

View file

@ -0,0 +1,15 @@
const isLogEnabled = (process.env.LDAP_LOG_ENABLED === 'true');
function log (level, message, data) {
if (isLogEnabled) {
console.log(`[${level}] ${message} ${ data ? JSON.stringify(data, null, 2) : '' }`);
}
}
function log_debug (...args) { log('DEBUG', ...args); }
function log_info (...args) { log('INFO', ...args); }
function log_warn (...args) { log('WARN', ...args); }
function log_error (...args) { log('ERROR', ...args); }
export { log, log_debug, log_info, log_warn, log_error };

View file

@ -0,0 +1,224 @@
import {slug, getLdapUsername, getLdapEmail, getLdapUserUniqueID, syncUserData, addLdapUser} from './sync';
import LDAP from './ldap';
import { log_debug, log_info, log_warn, log_error } from './logger';
function fallbackDefaultAccountSystem(bind, username, password) {
if (typeof username === 'string') {
if (username.indexOf('@') === -1) {
username = {username};
} else {
username = {email: username};
}
}
log_info('Fallback to default account system: ', username );
const loginRequest = {
user: username,
password: {
digest: SHA256(password),
algorithm: 'sha-256',
},
};
log_debug('Fallback options: ', loginRequest);
return Accounts._runLoginHandlers(bind, loginRequest);
}
Accounts.registerLoginHandler('ldap', function(loginRequest) {
if (!loginRequest.ldap || !loginRequest.ldapOptions) {
return undefined;
}
log_info('Init LDAP login', loginRequest.username);
if (LDAP.settings_get('LDAP_ENABLE') !== true) {
return fallbackDefaultAccountSystem(this, loginRequest.username, loginRequest.ldapPass);
}
const self = this;
const ldap = new LDAP();
let ldapUser;
try {
ldap.connectSync();
const users = ldap.searchUsersSync(loginRequest.username);
if (users.length !== 1) {
log_info('Search returned', users.length, 'record(s) for', loginRequest.username);
throw new Error('User not Found');
}
if (ldap.authSync(users[0].dn, loginRequest.ldapPass) === true) {
if (ldap.isUserInGroup(loginRequest.username, users[0])) {
ldapUser = users[0];
} else {
throw new Error('User not in a valid group');
}
} else {
log_info('Wrong password for', loginRequest.username);
}
} catch (error) {
log_error(error);
}
if (ldapUser === undefined) {
if (LDAP.settings_get('LDAP_LOGIN_FALLBACK') === true) {
return fallbackDefaultAccountSystem(self, loginRequest.username, loginRequest.ldapPass);
}
throw new Meteor.Error('LDAP-login-error', `LDAP Authentication failed with provided username [${ loginRequest.username }]`);
}
// Look to see if user already exists
let userQuery;
const Unique_Identifier_Field = getLdapUserUniqueID(ldapUser);
let user;
// Attempt to find user by unique identifier
if (Unique_Identifier_Field) {
userQuery = {
'services.ldap.id': Unique_Identifier_Field.value,
};
log_info('Querying user');
log_debug('userQuery', userQuery);
user = Meteor.users.findOne(userQuery);
}
// Attempt to find user by username
let username;
let email;
if (LDAP.settings_get('LDAP_USERNAME_FIELD') !== '') {
username = slug(getLdapUsername(ldapUser));
} else {
username = slug(loginRequest.username);
}
if(LDAP.settings_get('LDAP_EMAIL_FIELD') !== '') {
email = getLdapEmail(ldapUser);
}
if (!user) {
if(email && LDAP.settings_get('LDAP_EMAIL_MATCH_REQUIRE') === true) {
if(LDAP.settings_get('LDAP_EMAIL_MATCH_VERIFIED') === true) {
userQuery = {
'_id' : username,
'emails.0.address' : email,
'emails.0.verified' : true
};
} else {
userQuery = {
'_id' : username,
'emails.0.address' : email
};
}
} else {
userQuery = {
username
};
}
log_debug('userQuery', userQuery);
user = Meteor.users.findOne(userQuery);
}
// Attempt to find user by e-mail address only
if (!user && email && LDAP.settings_get('LDAP_EMAIL_MATCH_ENABLE') === true) {
log_info('No user exists with username', username, '- attempting to find by e-mail address instead');
if(LDAP.settings_get('LDAP_EMAIL_MATCH_VERIFIED') === true) {
userQuery = {
'emails.0.address': email,
'emails.0.verified' : true
};
} else {
userQuery = {
'emails.0.address' : email
};
}
log_debug('userQuery', userQuery);
user = Meteor.users.findOne(userQuery);
}
// Login user if they exist
if (user) {
if (user.authenticationMethod !== 'ldap' && LDAP.settings_get('LDAP_MERGE_EXISTING_USERS') !== true) {
log_info('User exists without "authenticationMethod : ldap"');
throw new Meteor.Error('LDAP-login-error', `LDAP Authentication succeded, but there's already a matching Wekan account in MongoDB`);
}
log_info('Logging user');
const stampedToken = Accounts._generateStampedLoginToken();
const update_data = {
$push: {
'services.resume.loginTokens': Accounts._hashStampedToken(stampedToken),
},
};
if( LDAP.settings_get('LDAP_SYNC_GROUP_ROLES') === true ) {
log_debug('Updating Groups/Roles');
const groups = ldap.getUserGroups(username, ldapUser);
if( groups.length > 0 ) {
Roles.setUserRoles(user._id, groups );
log_info(`Updated roles to:${ groups.join(',')}`);
}
}
Meteor.users.update(user._id, update_data );
syncUserData(user, ldapUser);
if (LDAP.settings_get('LDAP_LOGIN_FALLBACK') === true) {
Accounts.setPassword(user._id, loginRequest.ldapPass, {logout: false});
}
return {
userId: user._id,
token: stampedToken.token,
};
}
// Create new user
log_info('User does not exist, creating', username);
if (LDAP.settings_get('LDAP_USERNAME_FIELD') === '') {
username = undefined;
}
if (LDAP.settings_get('LDAP_LOGIN_FALLBACK') !== true) {
loginRequest.ldapPass = undefined;
}
const result = addLdapUser(ldapUser, username, loginRequest.ldapPass);
if( LDAP.settings_get('LDAP_SYNC_GROUP_ROLES') === true ) {
const groups = ldap.getUserGroups(username, ldapUser);
if( groups.length > 0 ) {
Roles.setUserRoles(result.userId, groups );
log_info(`Set roles to:${ groups.join(',')}`);
}
}
if (result instanceof Error) {
throw result;
}
return result;
});

View file

@ -0,0 +1,447 @@
import _ from 'underscore';
import LDAP from './ldap';
import { log_debug, log_info, log_warn, log_error } from './logger';
Object.defineProperty(Object.prototype, "getLDAPValue", {
value: function (prop) {
const self = this;
for (let key in self) {
if (key.toLowerCase() == prop.toLowerCase()) {
return self[key];
}
}
},
enumerable: false
});
export function slug(text) {
if (LDAP.settings_get('LDAP_UTF8_NAMES_SLUGIFY') !== true) {
return text;
}
text = slugify(text, '.');
return text.replace(/[^0-9a-z-_.]/g, '');
}
function templateVarHandler (variable, object) {
const templateRegex = /#{([\w\-]+)}/gi;
let match = templateRegex.exec(variable);
let tmpVariable = variable;
if (match == null) {
if (!object.hasOwnProperty(variable)) {
return;
}
return object[variable];
} else {
while (match != null) {
const tmplVar = match[0];
const tmplAttrName = match[1];
if (!object.hasOwnProperty(tmplAttrName)) {
return;
}
const attrVal = object[tmplAttrName];
tmpVariable = tmpVariable.replace(tmplVar, attrVal);
match = templateRegex.exec(variable);
}
return tmpVariable;
}
}
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 = LDAP.settings_get('LDAP_USERNAME_FIELD');
if (usernameField.indexOf('#{') > -1) {
return usernameField.replace(/#{(.+?)}/g, function(match, field) {
return ldapUser.getLDAPValue(field);
});
}
return ldapUser.getLDAPValue(usernameField);
}
export function getLdapEmail(ldapUser) {
const emailField = LDAP.settings_get('LDAP_EMAIL_FIELD');
if (emailField.indexOf('#{') > -1) {
return emailField.replace(/#{(.+?)}/g, function(match, field) {
return ldapUser.getLDAPValue(field);
});
}
return ldapUser.getLDAPValue(emailField);
}
export function getLdapFullname(ldapUser) {
const fullnameField = LDAP.settings_get('LDAP_FULLNAME_FIELD');
if (fullnameField.indexOf('#{') > -1) {
return fullnameField.replace(/#{(.+?)}/g, function(match, field) {
return ldapUser.getLDAPValue(field);
});
}
return ldapUser.getLDAPValue(fullnameField);
}
export function getLdapUserUniqueID(ldapUser) {
let Unique_Identifier_Field = LDAP.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 = LDAP.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) => {
return !_.isEmpty(ldapUser._raw.getLDAPValue(field));
});
if (Unique_Identifier_Field) {
log_debug(`Identifying user with: ${ Unique_Identifier_Field}`);
Unique_Identifier_Field = {
attribute: Unique_Identifier_Field,
value: ldapUser._raw.getLDAPValue(Unique_Identifier_Field).toString('hex'),
};
}
return Unique_Identifier_Field;
}
}
export function getDataToSyncUserData(ldapUser, user) {
const syncUserData = LDAP.settings_get('LDAP_SYNC_USER_DATA');
const syncUserDataFieldMap = LDAP.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) {
log_debug(`Mapping field ${ldapField} -> ${userField}`);
switch (userField) {
case 'email':
if (!ldapUser.hasOwnProperty(ldapField)) {
log_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)) {
log_debug(`user attribute not whitelisted: ${ userField }`);
return;
}
if (outerKey === 'customFields') {
let customFieldsMeta;
try {
customFieldsMeta = JSON.parse(LDAP.settings_get('Accounts_CustomFields'));
} catch (e) {
log_debug('Invalid JSON for Custom Fields');
return;
}
if (!getPropertyValue(customFieldsMeta, innerKeys)) {
log_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) =>
(currKey === lastKey)
? obj[currKey] = tmpLdapField
: obj[currKey] = obj[currKey] || {}
, userData);
log_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.authenticationMethod !== 'ldap') {
userData.ldap = true;
}
if (_.size(userData)) {
return userData;
}
}
export function syncUserData(user, ldapUser) {
log_info('Syncing user data');
log_debug('user', {'email': user.email, '_id': user._id});
// log_debug('ldapUser', ldapUser.object);
if (LDAP.settings_get('LDAP_USERNAME_FIELD') !== '') {
const username = slug(getLdapUsername(ldapUser));
if (user && user._id && username !== user.username) {
log_info('Syncing user username', user.username, '->', username);
Meteor.users.findOne({ _id: user._id }, { $set: { username }});
}
}
if (LDAP.settings_get('LDAP_FULLNAME_FIELD') !== '') {
const fullname= getLdapFullname(ldapUser);
log_debug('fullname=',fullname);
if (user && user._id && fullname !== '') {
log_info('Syncing user fullname:', fullname);
Meteor.users.update({ _id: user._id }, { $set: { 'profile.fullname' : fullname, }});
}
}
}
export function addLdapUser(ldapUser, username, password) {
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 (LDAP.settings_get('LDAP_DEFAULT_DOMAIN') !== '') {
userObject.email = `${ username || uniqueId.value }@${ LDAP.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?');
log_error(error);
throw error;
}
log_debug('New user data', userObject);
if (password) {
userObject.password = password;
}
try {
// This creates the account with password service
userObject.ldap = true;
userObject._id = Accounts.createUser(userObject);
// Add the services.ldap identifiers
Meteor.users.update({ _id: userObject._id }, {
$set: {
'services.ldap': { id: uniqueId.value },
'emails.0.verified': true,
'authenticationMethod': 'ldap',
}});
} catch (error) {
log_error('Error creating user', error);
return error;
}
syncUserData(userObject, ldapUser);
return {
userId: userObject._id,
};
}
export function importNewUsers(ldap) {
if (LDAP.settings_get('LDAP_ENABLE') !== true) {
log_error('Can\'t run LDAP Import, LDAP is disabled');
return;
}
if (!ldap) {
ldap = new LDAP();
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,
};
log_debug('userQuery', userQuery);
let username;
if (LDAP.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 && LDAP.settings_get('LDAP_MERGE_EXISTING_USERS') === true) {
const userQuery = {
username,
};
log_debug('userQuery merge', userQuery);
user = Meteor.users.findOne(userQuery);
if (user) {
syncUserData(user, ldapUser);
}
}
if (!user) {
addLdapUser(ldapUser, username);
}
if (count % 100 === 0) {
log_info('Import running. Users imported until now:', count);
}
});
if (end) {
log_info('Import finished. Users imported:', count);
}
next(count);
}));
}
function sync() {
if (LDAP.settings_get('LDAP_ENABLE') !== true) {
return;
}
const ldap = new LDAP();
try {
ldap.connectSync();
let users;
if (LDAP.settings_get('LDAP_BACKGROUND_SYNC_KEEP_EXISTANT_USERS_UPDATED') === true) {
users = Meteor.users.find({ 'services.ldap': { $exists: true }});
}
if (LDAP.settings_get('LDAP_BACKGROUND_SYNC_IMPORT_NEW_USERS') === true) {
importNewUsers(ldap);
}
if (LDAP.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);
} else {
log_info('Can\'t sync user', user.username);
}
});
}
} catch (error) {
log_error(error);
return error;
}
return true;
}
const jobName = 'LDAP_Sync';
const addCronJob = _.debounce(Meteor.bindEnvironment(function addCronJobDebounced() {
if (LDAP.settings_get('LDAP_BACKGROUND_SYNC') !== true) {
log_info('Disabling LDAP Background Sync');
if (SyncedCron.nextScheduledAtDate(jobName)) {
SyncedCron.remove(jobName);
}
return;
}
if (LDAP.settings_get('LDAP_BACKGROUND_SYNC_INTERVAL')) {
log_info('Enabling LDAP Background Sync');
SyncedCron.add({
name: jobName,
schedule: (parser) => parser.text(LDAP.settings_get('LDAP_BACKGROUND_SYNC_INTERVAL')),
job() {
sync();
},
});
SyncedCron.start();
}
}), 500);
Meteor.startup(() => {
Meteor.defer(() => {
LDAP.settings_get('LDAP_BACKGROUND_SYNC', addCronJob);
LDAP.settings_get('LDAP_BACKGROUND_SYNC_INTERVAL', addCronJob);
});
});

View file

@ -0,0 +1,29 @@
import {importNewUsers} from './sync';
import LDAP from './ldap';
Meteor.methods({
ldap_sync_now() {
const user = Meteor.user();
if (!user) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'ldap_sync_users' });
}
//TODO: This needs to be fixed - security issue -> alanning:meteor-roles
//if (!RocketChat.authz.hasRole(user._id, 'admin')) {
// throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'ldap_sync_users' });
//}
if (LDAP.settings_get('LDAP_ENABLE') !== true) {
throw new Meteor.Error('LDAP_disabled');
}
this.unblock();
importNewUsers();
return {
message: 'Sync_in_progress',
params: [],
};
},
});

View file

@ -0,0 +1,39 @@
import LDAP from './ldap';
Meteor.methods({
ldap_test_connection() {
const user = Meteor.user();
if (!user) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'ldap_test_connection' });
}
//TODO: This needs to be fixed - security issue -> alanning:meteor-roles
//if (!RocketChat.authz.hasRole(user._id, 'admin')) {
// throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'ldap_test_connection' });
//}
if (LDAP.settings_get(LDAP_ENABLE) !== true) {
throw new Meteor.Error('LDAP_disabled');
}
let ldap;
try {
ldap = new LDAP();
ldap.connectSync();
} catch (error) {
console.log(error);
throw new Meteor.Error(error.message);
}
try {
ldap.bindIfNecessary();
} catch (error) {
throw new Meteor.Error(error.name || error.message);
}
return {
message: 'Connection_success',
params: [],
};
},
});