summaryrefslogtreecommitdiffstats
path: root/packages/wekan-ldap
diff options
context:
space:
mode:
authorLauri Ojansivu <x@xet7.org>2019-04-20 15:18:33 +0300
committerLauri Ojansivu <x@xet7.org>2019-04-20 15:18:33 +0300
commit73e265d8fd050ae3daa67472b4465a5c49d68910 (patch)
tree677b233934a43d8f873e24c794ce289d85e3a9b7 /packages/wekan-ldap
parent6117097a93bfb11c8bd4c87a23c44a50e22ceb87 (diff)
downloadwekan-73e265d8fd050ae3daa67472b4465a5c49d68910.tar.gz
wekan-73e265d8fd050ae3daa67472b4465a5c49d68910.tar.bz2
wekan-73e265d8fd050ae3daa67472b4465a5c49d68910.zip
Include to Wekan packages directory contents, so that meteor command would build all directly.
This also simplifies build scripts. Thanks to xet7 !
Diffstat (limited to 'packages/wekan-ldap')
-rw-r--r--packages/wekan-ldap/LICENSE21
-rw-r--r--packages/wekan-ldap/README.md130
-rw-r--r--packages/wekan-ldap/client/loginHelper.js52
-rw-r--r--packages/wekan-ldap/package.js32
-rw-r--r--packages/wekan-ldap/server/index.js1
-rw-r--r--packages/wekan-ldap/server/ldap.js555
-rw-r--r--packages/wekan-ldap/server/logger.js15
-rw-r--r--packages/wekan-ldap/server/loginHandler.js224
-rw-r--r--packages/wekan-ldap/server/sync.js447
-rw-r--r--packages/wekan-ldap/server/syncUser.js29
-rw-r--r--packages/wekan-ldap/server/testConnection.js39
11 files changed, 1545 insertions, 0 deletions
diff --git a/packages/wekan-ldap/LICENSE b/packages/wekan-ldap/LICENSE
new file mode 100644
index 00000000..c2d69158
--- /dev/null
+++ b/packages/wekan-ldap/LICENSE
@@ -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.
diff --git a/packages/wekan-ldap/README.md b/packages/wekan-ldap/README.md
new file mode 100644
index 00000000..4f41d023
--- /dev/null
+++ b/packages/wekan-ldap/README.md
@@ -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
+}
+```
diff --git a/packages/wekan-ldap/client/loginHelper.js b/packages/wekan-ldap/client/loginHelper.js
new file mode 100644
index 00000000..48a290c1
--- /dev/null
+++ b/packages/wekan-ldap/client/loginHelper.js
@@ -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();
+ }
+ },
+ });
+};
diff --git a/packages/wekan-ldap/package.js b/packages/wekan-ldap/package.js
new file mode 100644
index 00000000..f6fbb458
--- /dev/null
+++ b/packages/wekan-ldap/package.js
@@ -0,0 +1,32 @@
+Package.describe({
+ name: 'wekan: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('1.0.3.1');
+ api.use('yasaricli:slugify@0.0.5');
+ api.use('ecmascript@0.9.0');
+ api.use('underscore');
+ api.use('sha');
+ api.use('templating', 'client');
+
+ api.use('accounts-base', 'server');
+ api.use('accounts-password', 'server');
+
+ api.addFiles('client/loginHelper.js', 'client');
+
+ api.mainModule('server/index.js', 'server');
+});
+
+Npm.depends({
+ ldapjs: '1.0.2',
+}); \ No newline at end of file
diff --git a/packages/wekan-ldap/server/index.js b/packages/wekan-ldap/server/index.js
new file mode 100644
index 00000000..e3ff85a1
--- /dev/null
+++ b/packages/wekan-ldap/server/index.js
@@ -0,0 +1 @@
+import './loginHandler';
diff --git a/packages/wekan-ldap/server/ldap.js b/packages/wekan-ldap/server/ldap.js
new file mode 100644
index 00000000..555a30aa
--- /dev/null
+++ b/packages/wekan-ldap/server/ldap.js
@@ -0,0 +1,555 @@
+import ldapjs from 'ldapjs';
+import util from 'util';
+import Bunyan from 'bunyan';
+import { log_debug, log_info, log_warn, log_error } from './logger';
+
+export default class LDAP {
+ constructor(){
+ this.ldapjs = ldapjs;
+
+ this.connected = false;
+
+ this.options = {
+ host: this.constructor.settings_get('LDAP_HOST'),
+ port: this.constructor.settings_get('LDAP_PORT'),
+ Reconnect: this.constructor.settings_get('LDAP_RECONNECT'),
+ timeout: this.constructor.settings_get('LDAP_TIMEOUT'),
+ connect_timeout: this.constructor.settings_get('LDAP_CONNECT_TIMEOUT'),
+ idle_timeout: this.constructor.settings_get('LDAP_IDLE_TIMEOUT'),
+ encryption: this.constructor.settings_get('LDAP_ENCRYPTION'),
+ ca_cert: this.constructor.settings_get('LDAP_CA_CERT'),
+ reject_unauthorized: this.constructor.settings_get('LDAP_REJECT_UNAUTHORIZED') || false,
+ Authentication: this.constructor.settings_get('LDAP_AUTHENTIFICATION'),
+ Authentication_UserDN: this.constructor.settings_get('LDAP_AUTHENTIFICATION_USERDN'),
+ Authentication_Password: this.constructor.settings_get('LDAP_AUTHENTIFICATION_PASSWORD'),
+ Authentication_Fallback: this.constructor.settings_get('LDAP_LOGIN_FALLBACK'),
+ BaseDN: this.constructor.settings_get('LDAP_BASEDN'),
+ Internal_Log_Level: this.constructor.settings_get('INTERNAL_LOG_LEVEL'),
+ User_Search_Filter: this.constructor.settings_get('LDAP_USER_SEARCH_FILTER'),
+ User_Search_Scope: this.constructor.settings_get('LDAP_USER_SEARCH_SCOPE'),
+ User_Search_Field: this.constructor.settings_get('LDAP_USER_SEARCH_FIELD'),
+ Search_Page_Size: this.constructor.settings_get('LDAP_SEARCH_PAGE_SIZE'),
+ Search_Size_Limit: this.constructor.settings_get('LDAP_SEARCH_SIZE_LIMIT'),
+ group_filter_enabled: this.constructor.settings_get('LDAP_GROUP_FILTER_ENABLE'),
+ group_filter_object_class: this.constructor.settings_get('LDAP_GROUP_FILTER_OBJECTCLASS'),
+ group_filter_group_id_attribute: this.constructor.settings_get('LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE'),
+ group_filter_group_member_attribute: this.constructor.settings_get('LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE'),
+ group_filter_group_member_format: this.constructor.settings_get('LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT'),
+ group_filter_group_name: this.constructor.settings_get('LDAP_GROUP_FILTER_GROUP_NAME'),
+ };
+ }
+
+ static settings_get(name, ...args) {
+ let value = process.env[name];
+ if (value !== undefined) {
+ if (value === 'true' || value === 'false') {
+ value = JSON.parse(value);
+ } else if (value !== '' && !isNaN(value)) {
+ value = Number(value);
+ }
+ return value;
+ } else {
+ log_warn(`Lookup for unset variable: ${name}`);
+ }
+ }
+ connectSync(...args) {
+ if (!this._connectSync) {
+ this._connectSync = Meteor.wrapAsync(this.connectAsync, this);
+ }
+ return this._connectSync(...args);
+ }
+
+ searchAllSync(...args) {
+ if (!this._searchAllSync) {
+ this._searchAllSync = Meteor.wrapAsync(this.searchAllAsync, this);
+ }
+ return this._searchAllSync(...args);
+ }
+
+ connectAsync(callback) {
+ log_info('Init setup');
+
+ let replied = false;
+
+ const connectionOptions = {
+ url: `${ this.options.host }:${ this.options.port }`,
+ timeout: this.options.timeout,
+ connectTimeout: this.options.connect_timeout,
+ idleTimeout: this.options.idle_timeout,
+ reconnect: this.options.Reconnect,
+ };
+
+ if (this.options.Internal_Log_Level !== 'disabled') {
+ connectionOptions.log = new Bunyan({
+ name: 'ldapjs',
+ component: 'client',
+ stream: process.stderr,
+ level: this.options.Internal_Log_Level,
+ });
+ }
+
+ const tlsOptions = {
+ rejectUnauthorized: this.options.reject_unauthorized,
+ };
+
+ if (this.options.ca_cert && this.options.ca_cert !== '') {
+ // Split CA cert into array of strings
+ const chainLines = this.constructor.settings_get('LDAP_CA_CERT').split('\n');
+ let cert = [];
+ const ca = [];
+ chainLines.forEach((line) => {
+ cert.push(line);
+ if (line.match(/-END CERTIFICATE-/)) {
+ ca.push(cert.join('\n'));
+ cert = [];
+ }
+ });
+ tlsOptions.ca = ca;
+ }
+
+ if (this.options.encryption === 'ssl') {
+ connectionOptions.url = `ldaps://${ connectionOptions.url }`;
+ connectionOptions.tlsOptions = tlsOptions;
+ } else {
+ connectionOptions.url = `ldap://${ connectionOptions.url }`;
+ }
+
+ log_info('Connecting', connectionOptions.url);
+ log_debug(`connectionOptions${ util.inspect(connectionOptions)}`);
+
+ this.client = ldapjs.createClient(connectionOptions);
+
+ this.bindSync = Meteor.wrapAsync(this.client.bind, this.client);
+
+ this.client.on('error', (error) => {
+ log_error('connection', error);
+ if (replied === false) {
+ replied = true;
+ callback(error, null);
+ }
+ });
+
+ this.client.on('idle', () => {
+ log_info('Idle');
+ this.disconnect();
+ });
+
+ this.client.on('close', () => {
+ log_info('Closed');
+ });
+
+ if (this.options.encryption === 'tls') {
+ // Set host parameter for tls.connect which is used by ldapjs starttls. This shouldn't be needed in newer nodejs versions (e.g v5.6.0).
+ // https://github.com/RocketChat/Rocket.Chat/issues/2035
+ // https://github.com/mcavage/node-ldapjs/issues/349
+ tlsOptions.host = this.options.host;
+
+ log_info('Starting TLS');
+ log_debug('tlsOptions', tlsOptions);
+
+ this.client.starttls(tlsOptions, null, (error, response) => {
+ if (error) {
+ log_error('TLS connection', error);
+ if (replied === false) {
+ replied = true;
+ callback(error, null);
+ }
+ return;
+ }
+
+ log_info('TLS connected');
+ this.connected = true;
+ if (replied === false) {
+ replied = true;
+ callback(null, response);
+ }
+ });
+ } else {
+ this.client.on('connect', (response) => {
+ log_info('LDAP connected');
+ this.connected = true;
+ if (replied === false) {
+ replied = true;
+ callback(null, response);
+ }
+ });
+ }
+
+ setTimeout(() => {
+ if (replied === false) {
+ log_error('connection time out', connectionOptions.connectTimeout);
+ replied = true;
+ callback(new Error('Timeout'));
+ }
+ }, connectionOptions.connectTimeout);
+ }
+
+ getUserFilter(username) {
+ const filter = [];
+
+ if (this.options.User_Search_Filter !== '') {
+ if (this.options.User_Search_Filter[0] === '(') {
+ filter.push(`${ this.options.User_Search_Filter }`);
+ } else {
+ filter.push(`(${ this.options.User_Search_Filter })`);
+ }
+ }
+
+ const usernameFilter = this.options.User_Search_Field.split(',').map((item) => `(${ item }=${ username })`);
+
+ if (usernameFilter.length === 0) {
+ log_error('LDAP_LDAP_User_Search_Field not defined');
+ } else if (usernameFilter.length === 1) {
+ filter.push(`${ usernameFilter[0] }`);
+ } else {
+ filter.push(`(|${ usernameFilter.join('') })`);
+ }
+
+ return `(&${ filter.join('') })`;
+ }
+
+ bindIfNecessary() {
+ if (this.domainBinded === true) {
+ return;
+ }
+
+ if (this.options.Authentication !== true) {
+ return;
+ }
+
+ log_info('Binding UserDN', this.options.Authentication_UserDN);
+ this.bindSync(this.options.Authentication_UserDN, this.options.Authentication_Password);
+ this.domainBinded = true;
+ }
+
+ searchUsersSync(username, page) {
+ this.bindIfNecessary();
+
+ const searchOptions = {
+ filter: this.getUserFilter(username),
+ scope: this.options.User_Search_Scope || 'sub',
+ sizeLimit: this.options.Search_Size_Limit,
+ };
+
+ if (this.options.Search_Page_Size > 0) {
+ searchOptions.paged = {
+ pageSize: this.options.Search_Page_Size,
+ pagePause: !!page,
+ };
+ }
+
+ log_info('Searching user', username);
+ log_debug('searchOptions', searchOptions);
+ log_debug('BaseDN', this.options.BaseDN);
+
+ if (page) {
+ return this.searchAllPaged(this.options.BaseDN, searchOptions, page);
+ }
+
+ return this.searchAllSync(this.options.BaseDN, searchOptions);
+ }
+
+ getUserByIdSync(id, attribute) {
+ this.bindIfNecessary();
+
+ const Unique_Identifier_Field = this.constructor.settings_get('LDAP_UNIQUE_IDENTIFIER_FIELD').split(',');
+
+ let filter;
+
+ if (attribute) {
+ filter = new this.ldapjs.filters.EqualityFilter({
+ attribute,
+ value: new Buffer(id, 'hex'),
+ });
+ } else {
+ const filters = [];
+ Unique_Identifier_Field.forEach((item) => {
+ filters.push(new this.ldapjs.filters.EqualityFilter({
+ attribute: item,
+ value: new Buffer(id, 'hex'),
+ }));
+ });
+
+ filter = new this.ldapjs.filters.OrFilter({filters});
+ }
+
+ const searchOptions = {
+ filter,
+ scope: 'sub',
+ };
+
+ log_info('Searching by id', id);
+ log_debug('search filter', searchOptions.filter.toString());
+ log_debug('BaseDN', this.options.BaseDN);
+
+ const result = this.searchAllSync(this.options.BaseDN, searchOptions);
+
+ if (!Array.isArray(result) || result.length === 0) {
+ return;
+ }
+
+ if (result.length > 1) {
+ log_error('Search by id', id, 'returned', result.length, 'records');
+ }
+
+ return result[0];
+ }
+
+ getUserByUsernameSync(username) {
+ this.bindIfNecessary();
+
+ const searchOptions = {
+ filter: this.getUserFilter(username),
+ scope: this.options.User_Search_Scope || 'sub',
+ };
+
+ log_info('Searching user', username);
+ log_debug('searchOptions', searchOptions);
+ log_debug('BaseDN', this.options.BaseDN);
+
+ const result = this.searchAllSync(this.options.BaseDN, searchOptions);
+
+ if (!Array.isArray(result) || result.length === 0) {
+ return;
+ }
+
+ if (result.length > 1) {
+ log_error('Search by username', username, 'returned', result.length, 'records');
+ }
+
+ return result[0];
+ }
+
+ getUserGroups(username, ldapUser){
+ if (!this.options.group_filter_enabled) {
+ return true;
+ }
+
+ const filter = ['(&'];
+
+ if (this.options.group_filter_object_class !== '') {
+ filter.push(`(objectclass=${ this.options.group_filter_object_class })`);
+ }
+
+ if (this.options.group_filter_group_member_attribute !== '') {
+ const format_value = ldapUser[this.options.group_filter_group_member_format];
+ if( format_value ) {
+ filter.push(`(${ this.options.group_filter_group_member_attribute }=${ format_value })`);
+ }
+ }
+
+ filter.push(')');
+
+ const searchOptions = {
+ filter: filter.join('').replace(/#{username}/g, username),
+ scope: 'sub',
+ };
+
+ log_debug('Group list filter LDAP:', searchOptions.filter);
+
+ const result = this.searchAllSync(this.options.BaseDN, searchOptions);
+
+ if (!Array.isArray(result) || result.length === 0) {
+ return [];
+ }
+
+ const grp_identifier = this.options.group_filter_group_id_attribute || 'cn';
+ const groups = [];
+ result.map((item) => {
+ groups.push( item[ grp_identifier ] );
+ });
+ log_debug(`Groups: ${ groups.join(', ')}`);
+ return groups;
+
+ }
+
+ isUserInGroup(username, ldapUser) {
+ if (!this.options.group_filter_enabled) {
+ return true;
+ }
+
+ const grps = this.getUserGroups(username, ldapUser);
+
+ const filter = ['(&'];
+
+ if (this.options.group_filter_object_class !== '') {
+ filter.push(`(objectclass=${ this.options.group_filter_object_class })`);
+ }
+
+ if (this.options.group_filter_group_member_attribute !== '') {
+ const format_value = ldapUser[this.options.group_filter_group_member_format];
+ if( format_value ) {
+ filter.push(`(${ this.options.group_filter_group_member_attribute }=${ format_value })`);
+ }
+ }
+
+ if (this.options.group_filter_group_id_attribute !== '') {
+ filter.push(`(${ this.options.group_filter_group_id_attribute }=${ this.options.group_filter_group_name })`);
+ }
+ filter.push(')');
+
+ const searchOptions = {
+ filter: filter.join('').replace(/#{username}/g, username),
+ scope: 'sub',
+ };
+
+ log_debug('Group filter LDAP:', searchOptions.filter);
+
+ const result = this.searchAllSync(this.options.BaseDN, searchOptions);
+
+ if (!Array.isArray(result) || result.length === 0) {
+ return false;
+ }
+ return true;
+ }
+
+ extractLdapEntryData(entry) {
+ const values = {
+ _raw: entry.raw,
+ };
+
+ Object.keys(values._raw).forEach((key) => {
+ const value = values._raw[key];
+
+ if (!['thumbnailPhoto', 'jpegPhoto'].includes(key)) {
+ if (value instanceof Buffer) {
+ values[key] = value.toString();
+ } else {
+ values[key] = value;
+ }
+ }
+ });
+
+ return values;
+ }
+
+ searchAllPaged(BaseDN, options, page) {
+ this.bindIfNecessary();
+
+ const processPage = ({entries, title, end, next}) => {
+ log_info(title);
+ // Force LDAP idle to wait the record processing
+ this.client._updateIdle(true);
+ page(null, entries, {end, next: () => {
+ // Reset idle timer
+ this.client._updateIdle();
+ next && next();
+ }});
+ };
+
+ this.client.search(BaseDN, options, (error, res) => {
+ if (error) {
+ log_error(error);
+ page(error);
+ return;
+ }
+
+ res.on('error', (error) => {
+ log_error(error);
+ page(error);
+ return;
+ });
+
+ let entries = [];
+
+ const internalPageSize = options.paged && options.paged.pageSize > 0 ? options.paged.pageSize * 2 : 500;
+
+ res.on('searchEntry', (entry) => {
+ entries.push(this.extractLdapEntryData(entry));
+
+ if (entries.length >= internalPageSize) {
+ processPage({
+ entries,
+ title: 'Internal Page',
+ end: false,
+ });
+ entries = [];
+ }
+ });
+
+ res.on('page', (result, next) => {
+ if (!next) {
+ this.client._updateIdle(true);
+ processPage({
+ entries,
+ title: 'Final Page',
+ end: true,
+ });
+ } else if (entries.length) {
+ log_info('Page');
+ processPage({
+ entries,
+ title: 'Page',
+ end: false,
+ next,
+ });
+ entries = [];
+ }
+ });
+
+ res.on('end', () => {
+ if (entries.length) {
+ processPage({
+ entries,
+ title: 'Final Page',
+ end: true,
+ });
+ entries = [];
+ }
+ });
+ });
+ }
+
+ searchAllAsync(BaseDN, options, callback) {
+ this.bindIfNecessary();
+
+ this.client.search(BaseDN, options, (error, res) => {
+ if (error) {
+ log_error(error);
+ callback(error);
+ return;
+ }
+
+ res.on('error', (error) => {
+ log_error(error);
+ callback(error);
+ return;
+ });
+
+ const entries = [];
+
+ res.on('searchEntry', (entry) => {
+ entries.push(this.extractLdapEntryData(entry));
+ });
+
+ res.on('end', () => {
+ log_info('Search result count', entries.length);
+ callback(null, entries);
+ });
+ });
+ }
+
+ authSync(dn, password) {
+ log_info('Authenticating', dn);
+
+ try {
+ if (password === '') {
+ throw new Error('Password is not provided');
+ }
+ this.bindSync(dn, password);
+ log_info('Authenticated', dn);
+ return true;
+ } catch (error) {
+ log_info('Not authenticated', dn);
+ log_debug('error', error);
+ return false;
+ }
+ }
+
+ disconnect() {
+ this.connected = false;
+ this.domainBinded = false;
+ log_info('Disconecting');
+ this.client.unbind();
+ }
+}
diff --git a/packages/wekan-ldap/server/logger.js b/packages/wekan-ldap/server/logger.js
new file mode 100644
index 00000000..afd77112
--- /dev/null
+++ b/packages/wekan-ldap/server/logger.js
@@ -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 };
diff --git a/packages/wekan-ldap/server/loginHandler.js b/packages/wekan-ldap/server/loginHandler.js
new file mode 100644
index 00000000..a8f013d7
--- /dev/null
+++ b/packages/wekan-ldap/server/loginHandler.js
@@ -0,0 +1,224 @@
+import {slug, getLdapUsername, getLdapEmail, getLdapUserUniqueID, syncUserData, addLdapUser} from './sync';
+import LDAP from './ldap';
+import { log_debug, log_info, log_warn, log_error } from './logger';
+
+function fallbackDefaultAccountSystem(bind, username, password) {
+ if (typeof username === 'string') {
+ if (username.indexOf('@') === -1) {
+ username = {username};
+ } else {
+ username = {email: username};
+ }
+ }
+
+ log_info('Fallback to default account system: ', username );
+
+ const loginRequest = {
+ user: username,
+ password: {
+ digest: SHA256(password),
+ algorithm: 'sha-256',
+ },
+ };
+ log_debug('Fallback options: ', loginRequest);
+
+ return Accounts._runLoginHandlers(bind, loginRequest);
+}
+
+Accounts.registerLoginHandler('ldap', function(loginRequest) {
+ if (!loginRequest.ldap || !loginRequest.ldapOptions) {
+ return undefined;
+ }
+
+ log_info('Init LDAP login', loginRequest.username);
+
+ if (LDAP.settings_get('LDAP_ENABLE') !== true) {
+ return fallbackDefaultAccountSystem(this, loginRequest.username, loginRequest.ldapPass);
+ }
+
+ const self = this;
+ const ldap = new LDAP();
+ let ldapUser;
+
+ try {
+ ldap.connectSync();
+ const users = ldap.searchUsersSync(loginRequest.username);
+
+ if (users.length !== 1) {
+ log_info('Search returned', users.length, 'record(s) for', loginRequest.username);
+ throw new Error('User not Found');
+ }
+
+ if (ldap.authSync(users[0].dn, loginRequest.ldapPass) === true) {
+ if (ldap.isUserInGroup(loginRequest.username, users[0])) {
+ ldapUser = users[0];
+ } else {
+ throw new Error('User not in a valid group');
+ }
+ } else {
+ log_info('Wrong password for', loginRequest.username);
+ }
+ } catch (error) {
+ log_error(error);
+ }
+
+ if (ldapUser === undefined) {
+ if (LDAP.settings_get('LDAP_LOGIN_FALLBACK') === true) {
+ return fallbackDefaultAccountSystem(self, loginRequest.username, loginRequest.ldapPass);
+ }
+
+ throw new Meteor.Error('LDAP-login-error', `LDAP Authentication failed with provided username [${ loginRequest.username }]`);
+ }
+
+ // Look to see if user already exists
+
+ let userQuery;
+
+ const Unique_Identifier_Field = getLdapUserUniqueID(ldapUser);
+ let user;
+
+ // Attempt to find user by unique identifier
+
+ if (Unique_Identifier_Field) {
+ userQuery = {
+ 'services.ldap.id': Unique_Identifier_Field.value,
+ };
+
+ log_info('Querying user');
+ log_debug('userQuery', userQuery);
+
+ user = Meteor.users.findOne(userQuery);
+ }
+
+ // Attempt to find user by username
+
+ let username;
+ let email;
+
+ if (LDAP.settings_get('LDAP_USERNAME_FIELD') !== '') {
+ username = slug(getLdapUsername(ldapUser));
+ } else {
+ username = slug(loginRequest.username);
+ }
+
+ if(LDAP.settings_get('LDAP_EMAIL_FIELD') !== '') {
+ email = getLdapEmail(ldapUser);
+ }
+
+ if (!user) {
+ if(email && LDAP.settings_get('LDAP_EMAIL_MATCH_REQUIRE') === true) {
+ if(LDAP.settings_get('LDAP_EMAIL_MATCH_VERIFIED') === true) {
+ userQuery = {
+ '_id' : username,
+ 'emails.0.address' : email,
+ 'emails.0.verified' : true
+ };
+ } else {
+ userQuery = {
+ '_id' : username,
+ 'emails.0.address' : email
+ };
+ }
+ } else {
+ userQuery = {
+ username
+ };
+ }
+
+ log_debug('userQuery', userQuery);
+
+ user = Meteor.users.findOne(userQuery);
+ }
+
+ // Attempt to find user by e-mail address only
+
+ if (!user && email && LDAP.settings_get('LDAP_EMAIL_MATCH_ENABLE') === true) {
+
+ log_info('No user exists with username', username, '- attempting to find by e-mail address instead');
+
+ if(LDAP.settings_get('LDAP_EMAIL_MATCH_VERIFIED') === true) {
+ userQuery = {
+ 'emails.0.address': email,
+ 'emails.0.verified' : true
+ };
+ } else {
+ userQuery = {
+ 'emails.0.address' : email
+ };
+ }
+
+ log_debug('userQuery', userQuery);
+
+ user = Meteor.users.findOne(userQuery);
+
+ }
+
+ // Login user if they exist
+ if (user) {
+ if (user.authenticationMethod !== 'ldap' && LDAP.settings_get('LDAP_MERGE_EXISTING_USERS') !== true) {
+ log_info('User exists without "authenticationMethod : ldap"');
+ throw new Meteor.Error('LDAP-login-error', `LDAP Authentication succeded, but there's already a matching Wekan account in MongoDB`);
+ }
+
+ log_info('Logging user');
+
+ const stampedToken = Accounts._generateStampedLoginToken();
+ const update_data = {
+ $push: {
+ 'services.resume.loginTokens': Accounts._hashStampedToken(stampedToken),
+ },
+ };
+
+ if( LDAP.settings_get('LDAP_SYNC_GROUP_ROLES') === true ) {
+ log_debug('Updating Groups/Roles');
+ const groups = ldap.getUserGroups(username, ldapUser);
+
+ if( groups.length > 0 ) {
+ Roles.setUserRoles(user._id, groups );
+ log_info(`Updated roles to:${ groups.join(',')}`);
+ }
+ }
+
+ Meteor.users.update(user._id, update_data );
+
+ syncUserData(user, ldapUser);
+
+ if (LDAP.settings_get('LDAP_LOGIN_FALLBACK') === true) {
+ Accounts.setPassword(user._id, loginRequest.ldapPass, {logout: false});
+ }
+
+ return {
+ userId: user._id,
+ token: stampedToken.token,
+ };
+ }
+
+ // Create new user
+
+ log_info('User does not exist, creating', username);
+
+ if (LDAP.settings_get('LDAP_USERNAME_FIELD') === '') {
+ username = undefined;
+ }
+
+ if (LDAP.settings_get('LDAP_LOGIN_FALLBACK') !== true) {
+ loginRequest.ldapPass = undefined;
+ }
+
+ const result = addLdapUser(ldapUser, username, loginRequest.ldapPass);
+
+ if( LDAP.settings_get('LDAP_SYNC_GROUP_ROLES') === true ) {
+ const groups = ldap.getUserGroups(username, ldapUser);
+ if( groups.length > 0 ) {
+ Roles.setUserRoles(result.userId, groups );
+ log_info(`Set roles to:${ groups.join(',')}`);
+ }
+ }
+
+
+ if (result instanceof Error) {
+ throw result;
+ }
+
+ return result;
+});
diff --git a/packages/wekan-ldap/server/sync.js b/packages/wekan-ldap/server/sync.js
new file mode 100644
index 00000000..141ef349
--- /dev/null
+++ b/packages/wekan-ldap/server/sync.js
@@ -0,0 +1,447 @@
+import _ from 'underscore';
+import LDAP from './ldap';
+import { log_debug, log_info, log_warn, log_error } from './logger';
+
+Object.defineProperty(Object.prototype, "getLDAPValue", {
+ value: function (prop) {
+ const self = this;
+ for (let key in self) {
+ if (key.toLowerCase() == prop.toLowerCase()) {
+ return self[key];
+ }
+ }
+ },
+
+ enumerable: false
+});
+
+export function slug(text) {
+ if (LDAP.settings_get('LDAP_UTF8_NAMES_SLUGIFY') !== true) {
+ return text;
+ }
+ text = slugify(text, '.');
+ return text.replace(/[^0-9a-z-_.]/g, '');
+}
+
+function templateVarHandler (variable, object) {
+
+ const templateRegex = /#{([\w\-]+)}/gi;
+ let match = templateRegex.exec(variable);
+ let tmpVariable = variable;
+
+ if (match == null) {
+ if (!object.hasOwnProperty(variable)) {
+ return;
+ }
+ return object[variable];
+ } else {
+ while (match != null) {
+ const tmplVar = match[0];
+ const tmplAttrName = match[1];
+
+ if (!object.hasOwnProperty(tmplAttrName)) {
+ return;
+ }
+
+ const attrVal = object[tmplAttrName];
+ tmpVariable = tmpVariable.replace(tmplVar, attrVal);
+ match = templateRegex.exec(variable);
+ }
+ return tmpVariable;
+ }
+}
+
+export function getPropertyValue(obj, key) {
+ try {
+ return _.reduce(key.split('.'), (acc, el) => acc[el], obj);
+ } catch (err) {
+ return undefined;
+ }
+}
+
+export function getLdapUsername(ldapUser) {
+ const usernameField = LDAP.settings_get('LDAP_USERNAME_FIELD');
+
+ if (usernameField.indexOf('#{') > -1) {
+ return usernameField.replace(/#{(.+?)}/g, function(match, field) {
+ return ldapUser.getLDAPValue(field);
+ });
+ }
+
+ return ldapUser.getLDAPValue(usernameField);
+}
+
+export function getLdapEmail(ldapUser) {
+ const emailField = LDAP.settings_get('LDAP_EMAIL_FIELD');
+
+ if (emailField.indexOf('#{') > -1) {
+ return emailField.replace(/#{(.+?)}/g, function(match, field) {
+ return ldapUser.getLDAPValue(field);
+ });
+ }
+
+ return ldapUser.getLDAPValue(emailField);
+}
+
+export function getLdapFullname(ldapUser) {
+ const fullnameField = LDAP.settings_get('LDAP_FULLNAME_FIELD');
+ if (fullnameField.indexOf('#{') > -1) {
+ return fullnameField.replace(/#{(.+?)}/g, function(match, field) {
+ return ldapUser.getLDAPValue(field);
+ });
+ }
+ return ldapUser.getLDAPValue(fullnameField);
+}
+
+export function getLdapUserUniqueID(ldapUser) {
+ let Unique_Identifier_Field = LDAP.settings_get('LDAP_UNIQUE_IDENTIFIER_FIELD');
+
+ if (Unique_Identifier_Field !== '') {
+ Unique_Identifier_Field = Unique_Identifier_Field.replace(/\s/g, '').split(',');
+ } else {
+ Unique_Identifier_Field = [];
+ }
+
+ let User_Search_Field = LDAP.settings_get('LDAP_USER_SEARCH_FIELD');
+
+ if (User_Search_Field !== '') {
+ User_Search_Field = User_Search_Field.replace(/\s/g, '').split(',');
+ } else {
+ User_Search_Field = [];
+ }
+
+ Unique_Identifier_Field = Unique_Identifier_Field.concat(User_Search_Field);
+
+ if (Unique_Identifier_Field.length > 0) {
+ Unique_Identifier_Field = Unique_Identifier_Field.find((field) => {
+ return !_.isEmpty(ldapUser._raw.getLDAPValue(field));
+ });
+ if (Unique_Identifier_Field) {
+ log_debug(`Identifying user with: ${ Unique_Identifier_Field}`);
+ Unique_Identifier_Field = {
+ attribute: Unique_Identifier_Field,
+ value: ldapUser._raw.getLDAPValue(Unique_Identifier_Field).toString('hex'),
+ };
+ }
+ return Unique_Identifier_Field;
+ }
+}
+
+export function getDataToSyncUserData(ldapUser, user) {
+ const syncUserData = LDAP.settings_get('LDAP_SYNC_USER_DATA');
+ const syncUserDataFieldMap = LDAP.settings_get('LDAP_SYNC_USER_DATA_FIELDMAP').trim();
+
+ const userData = {};
+
+ if (syncUserData && syncUserDataFieldMap) {
+ const whitelistedUserFields = ['email', 'name', 'customFields'];
+ const fieldMap = JSON.parse(syncUserDataFieldMap);
+ const emailList = [];
+ _.map(fieldMap, function(userField, ldapField) {
+ log_debug(`Mapping field ${ldapField} -> ${userField}`);
+ switch (userField) {
+ case 'email':
+ if (!ldapUser.hasOwnProperty(ldapField)) {
+ log_debug(`user does not have attribute: ${ ldapField }`);
+ return;
+ }
+
+ if (_.isObject(ldapUser[ldapField])) {
+ _.map(ldapUser[ldapField], function(item) {
+ emailList.push({ address: item, verified: true });
+ });
+ } else {
+ emailList.push({ address: ldapUser[ldapField], verified: true });
+ }
+ break;
+
+ default:
+ const [outerKey, innerKeys] = userField.split(/\.(.+)/);
+
+ if (!_.find(whitelistedUserFields, (el) => el === outerKey)) {
+ log_debug(`user attribute not whitelisted: ${ userField }`);
+ return;
+ }
+
+ if (outerKey === 'customFields') {
+ let customFieldsMeta;
+
+ try {
+ customFieldsMeta = JSON.parse(LDAP.settings_get('Accounts_CustomFields'));
+ } catch (e) {
+ log_debug('Invalid JSON for Custom Fields');
+ return;
+ }
+
+ if (!getPropertyValue(customFieldsMeta, innerKeys)) {
+ log_debug(`user attribute does not exist: ${ userField }`);
+ return;
+ }
+ }
+
+ const tmpUserField = getPropertyValue(user, userField);
+ const tmpLdapField = templateVarHandler(ldapField, ldapUser);
+
+ if (tmpLdapField && tmpUserField !== tmpLdapField) {
+ // creates the object structure instead of just assigning 'tmpLdapField' to
+ // 'userData[userField]' in order to avoid the "cannot use the part (...)
+ // to traverse the element" (MongoDB) error that can happen. Do not handle
+ // arrays.
+ // TODO: Find a better solution.
+ const dKeys = userField.split('.');
+ const lastKey = _.last(dKeys);
+ _.reduce(dKeys, (obj, currKey) =>
+ (currKey === lastKey)
+ ? obj[currKey] = tmpLdapField
+ : obj[currKey] = obj[currKey] || {}
+ , userData);
+ log_debug(`user.${ userField } changed to: ${ tmpLdapField }`);
+ }
+ }
+ });
+
+ if (emailList.length > 0) {
+ if (JSON.stringify(user.emails) !== JSON.stringify(emailList)) {
+ userData.emails = emailList;
+ }
+ }
+ }
+
+ const uniqueId = getLdapUserUniqueID(ldapUser);
+
+ if (uniqueId && (!user.services || !user.services.ldap || user.services.ldap.id !== uniqueId.value || user.services.ldap.idAttribute !== uniqueId.attribute)) {
+ userData['services.ldap.id'] = uniqueId.value;
+ userData['services.ldap.idAttribute'] = uniqueId.attribute;
+ }
+
+ if (user.authenticationMethod !== 'ldap') {
+ userData.ldap = true;
+ }
+
+ if (_.size(userData)) {
+ return userData;
+ }
+}
+
+
+export function syncUserData(user, ldapUser) {
+ log_info('Syncing user data');
+ log_debug('user', {'email': user.email, '_id': user._id});
+ // log_debug('ldapUser', ldapUser.object);
+
+ if (LDAP.settings_get('LDAP_USERNAME_FIELD') !== '') {
+ const username = slug(getLdapUsername(ldapUser));
+ if (user && user._id && username !== user.username) {
+ log_info('Syncing user username', user.username, '->', username);
+ Meteor.users.findOne({ _id: user._id }, { $set: { username }});
+ }
+ }
+
+ if (LDAP.settings_get('LDAP_FULLNAME_FIELD') !== '') {
+ const fullname= getLdapFullname(ldapUser);
+ log_debug('fullname=',fullname);
+ if (user && user._id && fullname !== '') {
+ log_info('Syncing user fullname:', fullname);
+ Meteor.users.update({ _id: user._id }, { $set: { 'profile.fullname' : fullname, }});
+ }
+ }
+
+}
+
+export function addLdapUser(ldapUser, username, password) {
+ const uniqueId = getLdapUserUniqueID(ldapUser);
+
+ const userObject = {
+ };
+
+ if (username) {
+ userObject.username = username;
+ }
+
+ const userData = getDataToSyncUserData(ldapUser, {});
+
+ if (userData && userData.emails && userData.emails[0] && userData.emails[0].address) {
+ if (Array.isArray(userData.emails[0].address)) {
+ userObject.email = userData.emails[0].address[0];
+ } else {
+ userObject.email = userData.emails[0].address;
+ }
+ } else if (ldapUser.mail && ldapUser.mail.indexOf('@') > -1) {
+ userObject.email = ldapUser.mail;
+ } else if (LDAP.settings_get('LDAP_DEFAULT_DOMAIN') !== '') {
+ userObject.email = `${ username || uniqueId.value }@${ LDAP.settings_get('LDAP_DEFAULT_DOMAIN') }`;
+ } else {
+ const error = new Meteor.Error('LDAP-login-error', 'LDAP Authentication succeded, there is no email to create an account. Have you tried setting your Default Domain in LDAP Settings?');
+ log_error(error);
+ throw error;
+ }
+
+ log_debug('New user data', userObject);
+
+ if (password) {
+ userObject.password = password;
+ }
+
+ try {
+ // This creates the account with password service
+ userObject.ldap = true;
+ userObject._id = Accounts.createUser(userObject);
+
+ // Add the services.ldap identifiers
+ Meteor.users.update({ _id: userObject._id }, {
+ $set: {
+ 'services.ldap': { id: uniqueId.value },
+ 'emails.0.verified': true,
+ 'authenticationMethod': 'ldap',
+ }});
+ } catch (error) {
+ log_error('Error creating user', error);
+ return error;
+ }
+
+ syncUserData(userObject, ldapUser);
+
+ return {
+ userId: userObject._id,
+ };
+}
+
+export function importNewUsers(ldap) {
+ if (LDAP.settings_get('LDAP_ENABLE') !== true) {
+ log_error('Can\'t run LDAP Import, LDAP is disabled');
+ return;
+ }
+
+ if (!ldap) {
+ ldap = new LDAP();
+ ldap.connectSync();
+ }
+
+ let count = 0;
+ ldap.searchUsersSync('*', Meteor.bindEnvironment((error, ldapUsers, {next, end} = {}) => {
+ if (error) {
+ throw error;
+ }
+
+ ldapUsers.forEach((ldapUser) => {
+ count++;
+
+ const uniqueId = getLdapUserUniqueID(ldapUser);
+ // Look to see if user already exists
+ const userQuery = {
+ 'services.ldap.id': uniqueId.value,
+ };
+
+ log_debug('userQuery', userQuery);
+
+ let username;
+ if (LDAP.settings_get('LDAP_USERNAME_FIELD') !== '') {
+ username = slug(getLdapUsername(ldapUser));
+ }
+
+ // Add user if it was not added before
+ let user = Meteor.users.findOne(userQuery);
+
+ if (!user && username && LDAP.settings_get('LDAP_MERGE_EXISTING_USERS') === true) {
+ const userQuery = {
+ username,
+ };
+
+ log_debug('userQuery merge', userQuery);
+
+ user = Meteor.users.findOne(userQuery);
+ if (user) {
+ syncUserData(user, ldapUser);
+ }
+ }
+
+ if (!user) {
+ addLdapUser(ldapUser, username);
+ }
+
+ if (count % 100 === 0) {
+ log_info('Import running. Users imported until now:', count);
+ }
+ });
+
+ if (end) {
+ log_info('Import finished. Users imported:', count);
+ }
+
+ next(count);
+ }));
+}
+
+function sync() {
+ if (LDAP.settings_get('LDAP_ENABLE') !== true) {
+ return;
+ }
+
+ const ldap = new LDAP();
+
+ try {
+ ldap.connectSync();
+
+ let users;
+ if (LDAP.settings_get('LDAP_BACKGROUND_SYNC_KEEP_EXISTANT_USERS_UPDATED') === true) {
+ users = Meteor.users.find({ 'services.ldap': { $exists: true }});
+ }
+
+ if (LDAP.settings_get('LDAP_BACKGROUND_SYNC_IMPORT_NEW_USERS') === true) {
+ importNewUsers(ldap);
+ }
+
+ if (LDAP.settings_get('LDAP_BACKGROUND_SYNC_KEEP_EXISTANT_USERS_UPDATED') === true) {
+ users.forEach(function(user) {
+ let ldapUser;
+
+ if (user.services && user.services.ldap && user.services.ldap.id) {
+ ldapUser = ldap.getUserByIdSync(user.services.ldap.id, user.services.ldap.idAttribute);
+ } else {
+ ldapUser = ldap.getUserByUsernameSync(user.username);
+ }
+
+ if (ldapUser) {
+ syncUserData(user, ldapUser);
+ } else {
+ log_info('Can\'t sync user', user.username);
+ }
+ });
+ }
+ } catch (error) {
+ log_error(error);
+ return error;
+ }
+ return true;
+}
+
+const jobName = 'LDAP_Sync';
+
+const addCronJob = _.debounce(Meteor.bindEnvironment(function addCronJobDebounced() {
+ if (LDAP.settings_get('LDAP_BACKGROUND_SYNC') !== true) {
+ log_info('Disabling LDAP Background Sync');
+ if (SyncedCron.nextScheduledAtDate(jobName)) {
+ SyncedCron.remove(jobName);
+ }
+ return;
+ }
+
+ if (LDAP.settings_get('LDAP_BACKGROUND_SYNC_INTERVAL')) {
+ log_info('Enabling LDAP Background Sync');
+ SyncedCron.add({
+ name: jobName,
+ schedule: (parser) => parser.text(LDAP.settings_get('LDAP_BACKGROUND_SYNC_INTERVAL')),
+ job() {
+ sync();
+ },
+ });
+ SyncedCron.start();
+ }
+}), 500);
+
+Meteor.startup(() => {
+ Meteor.defer(() => {
+ LDAP.settings_get('LDAP_BACKGROUND_SYNC', addCronJob);
+ LDAP.settings_get('LDAP_BACKGROUND_SYNC_INTERVAL', addCronJob);
+ });
+});
diff --git a/packages/wekan-ldap/server/syncUser.js b/packages/wekan-ldap/server/syncUser.js
new file mode 100644
index 00000000..763ea836
--- /dev/null
+++ b/packages/wekan-ldap/server/syncUser.js
@@ -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: [],
+ };
+ },
+});
diff --git a/packages/wekan-ldap/server/testConnection.js b/packages/wekan-ldap/server/testConnection.js
new file mode 100644
index 00000000..02866ce5
--- /dev/null
+++ b/packages/wekan-ldap/server/testConnection.js
@@ -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: [],
+ };
+ },
+});