Added back WeKan lockout, ldap, oidc, cas.

Thanks to xet7 !
This commit is contained in:
Lauri Ojansivu 2022-05-16 21:12:27 +03:00
parent a73a4c1e5b
commit 00768b4392
45 changed files with 3966 additions and 0 deletions

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014-2019 The Wekan Team
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,130 @@
# meteor-ldap
This packages is based on the RocketChat ldap login package
# settings definition
LDAP_Enable: Self explanatory
LDAP_Port: The port of the LDAP server
LDAP_Host: The host server for the LDAP server
LDAP_BaseDN: The base DN for the LDAP Tree
LDAP_Login_Fallback: Fallback on the default authentication method
LDAP_Reconnect: Reconnect to the server if the connection is lost
LDAP_Timeout: self explanatory
LDAP_Idle_Timeout: self explanatory
LDAP_Connect_Timeout: self explanatory
LDAP_Authentication: If the LDAP needs a user account to search
LDAP_Authentication_UserDN: The search user DN
LDAP_Authentication_Password: The password for the search user
LDAP_Internal_Log_Level: The logging level for the module
LDAP_Background_Sync: If the sync of the users should be done in the
background
LDAP_Background_Sync_Interval: At which interval does the background task sync
LDAP_Encryption: If using LDAPS, set it to 'ssl', else it will use 'ldap://'
LDAP_CA_Cert: The certification for the LDAPS server
LDAP_Reject_Unauthorized: Reject Unauthorized Certificate
LDAP_User_Search_Filter:
LDAP_User_Search_Scope:
LDAP_User_Search_Field: Which field is used to find the user
LDAP_Search_Page_Size:
LDAP_Search_Size_Limit:
LDAP_Group_Filter_Enable: enable group filtering
LDAP_Group_Filter_ObjectClass: The object class for filtering
LDAP_Group_Filter_Group_Id_Attribute:
LDAP_Group_Filter_Group_Member_Attribute:
LDAP_Group_Filter_Group_Member_Format:
LDAP_Group_Filter_Group_Name:
LDAP_Unique_Identifier_Field: This field is sometimes class GUID ( Globally Unique Identifier)
UTF8_Names_Slugify: Convert the username to utf8
LDAP_Username_Field: Which field contains the ldap username
LDAP_Fullname_Field: Which field contains the ldap full name
LDAP_Email_Match_Enable: Allow existing account matching by e-mail address when username does not match
LDAP_Email_Match_Require: Require existing account matching by e-mail address when username does match
LDAP_Email_Match_Verified: Require existing account email address to be verified for matching
LDAP_Email_Field: Which field contains the LDAP e-mail address
LDAP_Sync_User_Data:
LDAP_Sync_User_Data_FieldMap:
Accounts_CustomFields:
LDAP_Default_Domain: The default domain of the ldap it is used to create email if the field is not map correctly with the LDAP_Sync_User_Data_FieldMap
# example settings.json
```
{
"LDAP_Port": 389,
"LDAP_Host": "localhost",
"LDAP_BaseDN": "ou=user,dc=example,dc=org",
"LDAP_Login_Fallback": false,
"LDAP_Reconnect": true,
"LDAP_Timeout": 10000,
"LDAP_Idle_Timeout": 10000,
"LDAP_Connect_Timeout": 10000,
"LDAP_Authentication": true,
"LDAP_Authentication_UserDN": "cn=admin,dc=example,dc=org",
"LDAP_Authentication_Password": "admin",
"LDAP_Internal_Log_Level": "debug",
"LDAP_Background_Sync": false,
"LDAP_Background_Sync_Interval": "100",
"LDAP_Encryption": false,
"LDAP_Reject_Unauthorized": false,
"LDAP_Group_Filter_Enable": false,
"LDAP_Search_Page_Size": 0,
"LDAP_Search_Size_Limit": 0,
"LDAP_User_Search_Filter": "",
"LDAP_User_Search_Field": "uid",
"LDAP_User_Search_Scope": "",
"LDAP_Unique_Identifier_Field": "guid",
"LDAP_Username_Field": "uid",
"LDAP_Fullname_Field": "cn",
"LDAP_Email_Match_Enable": true,
"LDAP_Email_Match_Require": false,
"LDAP_Email_Match_Verified": false,
"LDAP_Email_Field": "mail",
"LDAP_Sync_User_Data": false,
"LDAP_Sync_User_Data_FieldMap": "{\"cn\":\"name\", \"mail\":\"email\"}",
"LDAP_Merge_Existing_Users": true,
"UTF8_Names_Slugify": true
}
```

View file

@ -0,0 +1,52 @@
// Pass in username, password as normal
// customLdapOptions should be passed in if you want to override LDAP_DEFAULTS
// on any particular call (if you have multiple ldap servers you'd like to connect to)
// You'll likely want to set the dn value here {dn: "..."}
Meteor.loginWithLDAP = function(username, password, customLdapOptions, callback) {
// Retrieve arguments as array
const args = [];
for (let i = 0; i < arguments.length; i++) {
args.push(arguments[i]);
}
// Pull username and password
username = args.shift();
password = args.shift();
// Check if last argument is a function
// if it is, pop it off and set callback to it
if (typeof args[args.length-1] === 'function') {
callback = args.pop();
} else {
callback = null;
}
// if args still holds options item, grab it
if (args.length > 0) {
customLdapOptions = args.shift();
} else {
customLdapOptions = {};
}
// Set up loginRequest object
const loginRequest = {
ldap: true,
username,
ldapPass: password,
ldapOptions: customLdapOptions,
};
Accounts.callLoginMethod({
// Call login method with ldap = true
// This will hook into our login handler for ldap
methodArguments: [loginRequest],
userCallback(error/*, result*/) {
if (error) {
if (callback) {
callback(error);
}
} else if (callback) {
callback();
}
},
});
};

View file

@ -0,0 +1,28 @@
Package.describe({
name: 'wekan-ldap',
version: '0.0.2',
// Brief, one-line summary of the package.
summary: 'Basic meteor login with ldap',
// URL to the Git repository containing the source code for this package.
git: 'https://github.com/wekan/wekan-ldap',
// By default, Meteor will default to using README.md for documentation.
// To avoid submitting documentation, set this field to null.
documentation: 'README.md'
});
Package.onUse(function(api) {
api.versionsFrom('2.7');
api.use('yasaricli:slugify');
api.use('ecmascript');
api.use('underscore');
api.use('sha');
api.use('templating', 'client');
api.use('accounts-base', 'server');
api.use('accounts-password', 'server');
api.use('percolate:synced-cron', 'server');
api.addFiles('client/loginHelper.js', 'client');
api.mainModule('server/index.js', 'server');
});

View file

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

View file

@ -0,0 +1,593 @@
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') !== undefined ? this.constructor.settings_get('LDAP_REJECT_UNAUTHORIZED') : true,
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_Authentication : this.constructor.settings_get('LDAP_USER_AUTHENTICATION'),
User_Authentication_Field : this.constructor.settings_get('LDAP_USER_AUTHENTICATION_FIELD'),
User_Attributes : this.constructor.settings_get('LDAP_USER_ATTRIBUTES'),
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'),
AD_Simple_Auth : this.constructor.settings_get('LDAP_AD_SIMPLE_AUTH'),
Default_Domain : this.constructor.settings_get('LDAP_DEFAULT_DOMAIN'),
};
}
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').replace(/\\n/g,'\n').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('')})`;
}
bindUserIfNecessary(username, password) {
if (this.domainBinded === true) {
return;
}
if (!this.options.User_Authentication) {
return;
}
/* if SimpleAuth is configured, the BaseDN is not needed */
if (!this.options.BaseDN && !this.options.AD_Simple_Auth) throw new Error('BaseDN is not provided');
var userDn = "";
if (this.options.AD_Simple_Auth === true || this.options.AD_Simple_Auth === 'true') {
userDn = `${username}@${this.options.Default_Domain}`;
} else {
userDn = `${this.options.User_Authentication_Field}=${username},${this.options.BaseDN}`;
}
log_info('Binding with User', userDn);
this.bindSync(userDn, password);
this.domainBinded = true;
}
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.User_Attributes) searchOptions.attributes = this.options.User_Attributes.split(',');
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: Buffer.from(id, 'hex'),
});
} else {
const filters = [];
Unique_Identifier_Field.forEach((item) => {
filters.push(new this.ldapjs.filters.EqualityFilter({
attribute: item,
value : Buffer.from(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,252 @@
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();
if (!!LDAP.settings_get('LDAP_USER_AUTHENTICATION')) {
ldap.bindUserIfNecessary(loginRequest.username, loginRequest.ldapPass);
ldapUser = ldap.searchUsersSync(loginRequest.username)[0];
} else {
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.isUserInGroup(loginRequest.username, users[0])) {
ldapUser = users[0];
} else {
throw new Error('User not in a valid group');
}
if (ldap.authSync(users[0].dn, loginRequest.ldapPass) !== true) {
ldapUser = null;
log_info('Wrong password for', loginRequest.username)
}
}
} catch (error) {
log_error(error);
}
if (!ldapUser) {
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_ADMIN_STATUS') === true) {
log_debug('Updating admin status');
const targetGroups = LDAP.settings_get('LDAP_SYNC_ADMIN_GROUPS').split(',');
const groups = ldap.getUserGroups(username, ldapUser).filter((value) => targetGroups.includes(value));
user.isAdmin = groups.length > 0;
Meteor.users.update({_id: user._id}, {$set: {isAdmin: user.isAdmin}});
}
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_ADMIN_STATUS') === true) {
log_debug('Updating admin status');
const targetGroups = LDAP.settings_get('LDAP_SYNC_ADMIN_GROUPS').split(',');
const groups = ldap.getUserGroups(username, ldapUser).filter((value) => targetGroups.includes(value));
result.isAdmin = groups.length > 0;
Meteor.users.update({_id: result.userId}, {$set: {isAdmin: result.isAdmin}});
}
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,474 @@
import _ from 'underscore';
import SyncedCron from 'meteor/percolate:synced-cron';
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);
});
}
const ldapMail = ldapUser.getLDAPValue(emailField);
if (typeof ldapMail === 'string') {
return ldapMail;
} else {
return ldapMail[0].toString();
}
}
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, }});
}
}
if (LDAP.settings_get('LDAP_EMAIL_FIELD') !== '') {
const email = getLdapEmail(ldapUser);
log_debug('email=', email);
if (user && user._id && email !== '') {
log_info('Syncing user email:', email);
Meteor.users.update({
_id: user._id
}, {
$set: {
'emails.0.address': email,
}
});
}
}
}
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() {
let sc=SyncedCron.SyncedCron; //Why ?? something must be wrong in the import
if (LDAP.settings_get('LDAP_BACKGROUND_SYNC') !== true) {
log_info('Disabling LDAP Background Sync');
if (sc.nextScheduledAtDate(jobName)) {
sc.remove(jobName);
}
return;
}
log_info('Enabling LDAP Background Sync');
sc.add({
name: jobName,
schedule: function(parser) {
if (LDAP.settings_get('LDAP_BACKGROUND_SYNC_INTERVAL')) {
return parser.text(LDAP.settings_get('LDAP_BACKGROUND_SYNC_INTERVAL'));
}
else {
return parser.recur().on(0).minute();
}},
job: function() {
sync();
},
});
sc.start();
}), 500);
Meteor.startup(() => {
Meteor.defer(() => {
if(LDAP.settings_get('LDAP_BACKGROUND_SYNC')){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: [],
};
},
});