mirror of
https://github.com/wekan/wekan.git
synced 2025-12-16 23:40:13 +01:00
Added back WeKan lockout, ldap, oidc, cas.
Thanks to xet7 !
This commit is contained in:
parent
a73a4c1e5b
commit
00768b4392
45 changed files with 3966 additions and 0 deletions
21
packages/wekan-ldap/LICENSE
Normal file
21
packages/wekan-ldap/LICENSE
Normal 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.
|
||||
130
packages/wekan-ldap/README.md
Normal file
130
packages/wekan-ldap/README.md
Normal 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
|
||||
}
|
||||
```
|
||||
52
packages/wekan-ldap/client/loginHelper.js
Normal file
52
packages/wekan-ldap/client/loginHelper.js
Normal 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();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
28
packages/wekan-ldap/package.js
Normal file
28
packages/wekan-ldap/package.js
Normal 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');
|
||||
});
|
||||
1
packages/wekan-ldap/server/index.js
Normal file
1
packages/wekan-ldap/server/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import './loginHandler';
|
||||
593
packages/wekan-ldap/server/ldap.js
Normal file
593
packages/wekan-ldap/server/ldap.js
Normal 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();
|
||||
}
|
||||
}
|
||||
15
packages/wekan-ldap/server/logger.js
Normal file
15
packages/wekan-ldap/server/logger.js
Normal 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 };
|
||||
252
packages/wekan-ldap/server/loginHandler.js
Normal file
252
packages/wekan-ldap/server/loginHandler.js
Normal 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;
|
||||
});
|
||||
474
packages/wekan-ldap/server/sync.js
Normal file
474
packages/wekan-ldap/server/sync.js
Normal 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();}
|
||||
});
|
||||
});
|
||||
29
packages/wekan-ldap/server/syncUser.js
Normal file
29
packages/wekan-ldap/server/syncUser.js
Normal 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: [],
|
||||
};
|
||||
},
|
||||
});
|
||||
39
packages/wekan-ldap/server/testConnection.js
Normal file
39
packages/wekan-ldap/server/testConnection.js
Normal 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: [],
|
||||
};
|
||||
},
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue