From 7d56dca80be9bc9242fe80758fcd525a45f99396 Mon Sep 17 00:00:00 2001 From: Harry Adel Date: Thu, 29 Jan 2026 19:58:23 +0200 Subject: [PATCH 1/3] Migrate wekan-ldap to async API for Meteor 3.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace Meteor.wrapAsync with native Promises - Convert all sync methods to async - Use async DB operations (findOneAsync, updateAsync) - Bump version 0.0.2 → 0.1.0 --- packages/wekan-ldap/package.js | 2 +- packages/wekan-ldap/server/ldap.js | 83 ++++++++++++-------- packages/wekan-ldap/server/loginHandler.js | 38 ++++----- packages/wekan-ldap/server/sync.js | 52 ++++++------ packages/wekan-ldap/server/syncUser.js | 4 +- packages/wekan-ldap/server/testConnection.js | 6 +- 6 files changed, 102 insertions(+), 83 deletions(-) diff --git a/packages/wekan-ldap/package.js b/packages/wekan-ldap/package.js index e3da7243b..5402afde6 100644 --- a/packages/wekan-ldap/package.js +++ b/packages/wekan-ldap/package.js @@ -1,6 +1,6 @@ Package.describe({ name: 'wekan-ldap', - version: '0.0.2', + version: '0.1.0', // 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. diff --git a/packages/wekan-ldap/server/ldap.js b/packages/wekan-ldap/server/ldap.js index 890b03b4e..3fd37f5c2 100644 --- a/packages/wekan-ldap/server/ldap.js +++ b/packages/wekan-ldap/server/ldap.js @@ -73,19 +73,40 @@ export default class LDAP { } } - connectSync(...args) { - if (!this._connectSync) { - this._connectSync = Meteor.wrapAsync(this.connectAsync, this); - } - return this._connectSync(...args); + async connect() { + return new Promise((resolve, reject) => { + this.connectAsync((error, result) => { + if (error) { + reject(error); + } else { + resolve(result); + } + }); + }); } - searchAllSync(...args) { + async searchAll(BaseDN, options) { + return new Promise((resolve, reject) => { + this.searchAllAsync(BaseDN, options, (error, result) => { + if (error) { + reject(error); + } else { + resolve(result); + } + }); + }); + } - if (!this._searchAllSync) { - this._searchAllSync = Meteor.wrapAsync(this.searchAllAsync, this); - } - return this._searchAllSync(...args); + async bind(dn, password) { + return new Promise((resolve, reject) => { + this.client.bind(dn, password, (error) => { + if (error) { + reject(error); + } else { + resolve(); + } + }); + }); } connectAsync(callback) { @@ -132,8 +153,6 @@ export default class LDAP { 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) { @@ -223,7 +242,7 @@ export default class LDAP { return `(&${filter.join('')})`; } - bindUserIfNecessary(username, password) { + async bindUserIfNecessary(username, password) { if (this.domainBinded === true) { return; @@ -247,11 +266,11 @@ export default class LDAP { Log.info(`Binding with User ${userDn}`); - this.bindSync(userDn, password); + await this.bind(userDn, password); this.domainBinded = true; } - bindIfNecessary() { + async bindIfNecessary() { if (this.domainBinded === true) { return; } @@ -262,12 +281,12 @@ export default class LDAP { Log.info(`Binding UserDN ${this.options.Authentication_UserDN}`); - this.bindSync(this.options.Authentication_UserDN, this.options.Authentication_Password); + await this.bind(this.options.Authentication_UserDN, this.options.Authentication_Password); this.domainBinded = true; } - searchUsersSync(username, page) { - this.bindIfNecessary(); + async searchUsers(username, page) { + await this.bindIfNecessary(); const searchOptions = { filter : this.getUserFilter(username), scope : this.options.User_Search_Scope || 'sub', @@ -291,11 +310,11 @@ export default class LDAP { return this.searchAllPaged(this.options.BaseDN, searchOptions, page); } - return this.searchAllSync(this.options.BaseDN, searchOptions); + return await this.searchAll(this.options.BaseDN, searchOptions); } - getUserByIdSync(id, attribute) { - this.bindIfNecessary(); + async getUserById(id, attribute) { + await this.bindIfNecessary(); const Unique_Identifier_Field = this.constructor.settings_get('LDAP_UNIQUE_IDENTIFIER_FIELD').split(','); @@ -327,7 +346,7 @@ export default class LDAP { Log.debug(`search filter ${searchOptions.filter.toString()}`); Log.debug(`BaseDN ${this.options.BaseDN}`); - const result = this.searchAllSync(this.options.BaseDN, searchOptions); + const result = await this.searchAll(this.options.BaseDN, searchOptions); if (!Array.isArray(result) || result.length === 0) { return; @@ -340,8 +359,8 @@ export default class LDAP { return result[0]; } - getUserByUsernameSync(username) { - this.bindIfNecessary(); + async getUserByUsername(username) { + await this.bindIfNecessary(); const searchOptions = { filter: this.getUserFilter(username), @@ -352,7 +371,7 @@ export default class LDAP { Log.debug(`searchOptions ${searchOptions}`); Log.debug(`BaseDN ${this.options.BaseDN}`); - const result = this.searchAllSync(this.options.BaseDN, searchOptions); + const result = await this.searchAll(this.options.BaseDN, searchOptions); if (!Array.isArray(result) || result.length === 0) { return; @@ -365,7 +384,7 @@ export default class LDAP { return result[0]; } - getUserGroups(username, ldapUser) { + async getUserGroups(username, ldapUser) { if (!this.options.group_filter_enabled) { return true; } @@ -394,7 +413,7 @@ export default class LDAP { Log.debug(`Group list filter LDAP: ${searchOptions.filter}`); - const result = this.searchAllSync(this.options.BaseDN, searchOptions); + const result = await this.searchAll(this.options.BaseDN, searchOptions); if (!Array.isArray(result) || result.length === 0) { return []; @@ -410,12 +429,12 @@ export default class LDAP { } - isUserInGroup(username, ldapUser) { + async isUserInGroup(username, ldapUser) { if (!this.options.group_filter_enabled) { return true; } - const grps = this.getUserGroups(username, ldapUser); + const grps = await this.getUserGroups(username, ldapUser); const filter = ['(&']; @@ -444,7 +463,7 @@ export default class LDAP { Log.debug(`Group filter LDAP: ${searchOptions.filter}`); - const result = this.searchAllSync(this.options.BaseDN, searchOptions); + const result = await this.searchAll(this.options.BaseDN, searchOptions); if (!Array.isArray(result) || result.length === 0) { return false; @@ -580,14 +599,14 @@ export default class LDAP { }); } - authSync(dn, password) { + async auth(dn, password) { Log.info(`Authenticating ${dn}`); try { if (password === '') { throw new Error('Password is not provided'); } - this.bindSync(dn, password); + await this.bind(dn, password); Log.info(`Authenticated ${dn}`); return true; } catch (error) { diff --git a/packages/wekan-ldap/server/loginHandler.js b/packages/wekan-ldap/server/loginHandler.js index 090ef9da3..8864b0169 100644 --- a/packages/wekan-ldap/server/loginHandler.js +++ b/packages/wekan-ldap/server/loginHandler.js @@ -25,7 +25,7 @@ function fallbackDefaultAccountSystem(bind, username, password) { return Accounts._runLoginHandlers(bind, loginRequest); } -Accounts.registerLoginHandler('ldap', function(loginRequest) { +Accounts.registerLoginHandler('ldap', async function(loginRequest) { if (!loginRequest.ldap || !loginRequest.ldapOptions) { return undefined; } @@ -42,27 +42,27 @@ Accounts.registerLoginHandler('ldap', function(loginRequest) { try { - ldap.connectSync(); + await ldap.connect(); if (!!LDAP.settings_get('LDAP_USER_AUTHENTICATION')) { - ldap.bindUserIfNecessary(loginRequest.username, loginRequest.ldapPass); - ldapUser = ldap.searchUsersSync(loginRequest.username)[0]; + await ldap.bindUserIfNecessary(loginRequest.username, loginRequest.ldapPass); + ldapUser = (await ldap.searchUsers(loginRequest.username))[0]; } else { - const users = ldap.searchUsersSync(loginRequest.username); + const users = await ldap.searchUsers(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])) { + if (await 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) { + if (await ldap.auth(users[0].dn, loginRequest.ldapPass) !== true) { ldapUser = null; log_info('Wrong password for', loginRequest.username) } @@ -96,7 +96,7 @@ Accounts.registerLoginHandler('ldap', function(loginRequest) { log_info('Querying user'); log_debug('userQuery', userQuery); - user = Meteor.users.findOne(userQuery); + user = await Meteor.users.findOneAsync(userQuery); } // Attempt to find user by username @@ -137,7 +137,7 @@ Accounts.registerLoginHandler('ldap', function(loginRequest) { log_debug('userQuery', userQuery); - user = Meteor.users.findOne(userQuery); + user = await Meteor.users.findOneAsync(userQuery); } // Attempt to find user by e-mail address only @@ -159,7 +159,7 @@ Accounts.registerLoginHandler('ldap', function(loginRequest) { log_debug('userQuery', userQuery); - user = Meteor.users.findOne(userQuery); + user = await Meteor.users.findOneAsync(userQuery); } @@ -182,15 +182,15 @@ Accounts.registerLoginHandler('ldap', function(loginRequest) { 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)); + const groups = (await ldap.getUserGroups(username, ldapUser)).filter((value) => targetGroups.includes(value)); user.isAdmin = groups.length > 0; - Meteor.users.update({_id: user._id}, {$set: {isAdmin: user.isAdmin}}); + await Meteor.users.updateAsync({_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); + const groups = await ldap.getUserGroups(username, ldapUser); if( groups.length > 0 ) { Roles.setUserRoles(user._id, groups ); @@ -198,9 +198,9 @@ Accounts.registerLoginHandler('ldap', function(loginRequest) { } } - Meteor.users.update(user._id, update_data ); + await Meteor.users.updateAsync(user._id, update_data ); - syncUserData(user, ldapUser); + await syncUserData(user, ldapUser); if (LDAP.settings_get('LDAP_LOGIN_FALLBACK') === true) { Accounts.setPassword(user._id, loginRequest.ldapPass, {logout: false}); @@ -224,19 +224,19 @@ Accounts.registerLoginHandler('ldap', function(loginRequest) { loginRequest.ldapPass = undefined; } - const result = addLdapUser(ldapUser, username, loginRequest.ldapPass); + const result = await 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)); + const groups = (await ldap.getUserGroups(username, ldapUser)).filter((value) => targetGroups.includes(value)); result.isAdmin = groups.length > 0; - Meteor.users.update({_id: result.userId}, {$set: {isAdmin: result.isAdmin}}); + await Meteor.users.updateAsync({_id: result.userId}, {$set: {isAdmin: result.isAdmin}}); } if( LDAP.settings_get('LDAP_SYNC_GROUP_ROLES') === true ) { - const groups = ldap.getUserGroups(username, ldapUser); + const groups = await ldap.getUserGroups(username, ldapUser); if( groups.length > 0 ) { Roles.setUserRoles(result.userId, groups ); log_info(`Set roles to:${ groups.join(',')}`); diff --git a/packages/wekan-ldap/server/sync.js b/packages/wekan-ldap/server/sync.js index 0da3e2246..d6da2a34e 100644 --- a/packages/wekan-ldap/server/sync.js +++ b/packages/wekan-ldap/server/sync.js @@ -230,7 +230,7 @@ export function getDataToSyncUserData(ldapUser, user) { } -export function syncUserData(user, ldapUser) { +export async function syncUserData(user, ldapUser) { log_info('Syncing user data'); log_debug('user', {'email': user.email, '_id': user._id}); // log_debug('ldapUser', ldapUser.object); @@ -239,7 +239,7 @@ export function syncUserData(user, ldapUser) { 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 }}); + await Meteor.users.findOneAsync({ _id: user._id }, { $set: { username }}); } } @@ -248,7 +248,7 @@ export function syncUserData(user, 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, }}); + await Meteor.users.updateAsync({ _id: user._id }, { $set: { 'profile.fullname' : fullname, }}); } } @@ -258,7 +258,7 @@ export function syncUserData(user, ldapUser) { if (user && user._id && email !== '') { log_info('Syncing user email:', email); - Meteor.users.update({ + await Meteor.users.updateAsync({ _id: user._id }, { $set: { @@ -270,7 +270,7 @@ export function syncUserData(user, ldapUser) { } -export function addLdapUser(ldapUser, username, password) { +export async function addLdapUser(ldapUser, username, password) { const uniqueId = getLdapUserUniqueID(ldapUser); const userObject = { @@ -310,7 +310,7 @@ export function addLdapUser(ldapUser, username, password) { userObject._id = Accounts.createUser(userObject); // Add the services.ldap identifiers - Meteor.users.update({ _id: userObject._id }, { + await Meteor.users.updateAsync({ _id: userObject._id }, { $set: { 'services.ldap': { id: uniqueId.value }, 'emails.0.verified': true, @@ -321,14 +321,14 @@ export function addLdapUser(ldapUser, username, password) { return error; } - syncUserData(userObject, ldapUser); + await syncUserData(userObject, ldapUser); return { userId: userObject._id, }; } -export function importNewUsers(ldap) { +export async function importNewUsers(ldap) { if (LDAP.settings_get('LDAP_ENABLE') !== true) { log_error('Can\'t run LDAP Import, LDAP is disabled'); return; @@ -336,16 +336,16 @@ export function importNewUsers(ldap) { if (!ldap) { ldap = new LDAP(); - ldap.connectSync(); + await ldap.connect(); } let count = 0; - ldap.searchUsersSync('*', Meteor.bindEnvironment((error, ldapUsers, {next, end} = {}) => { + ldap.searchUsers('*', Meteor.bindEnvironment(async (error, ldapUsers, {next, end} = {}) => { if (error) { throw error; } - ldapUsers.forEach((ldapUser) => { + for (const ldapUser of ldapUsers) { count++; const uniqueId = getLdapUserUniqueID(ldapUser); @@ -362,7 +362,7 @@ export function importNewUsers(ldap) { } // Add user if it was not added before - let user = Meteor.users.findOne(userQuery); + let user = await Meteor.users.findOneAsync(userQuery); if (!user && username && LDAP.settings_get('LDAP_MERGE_EXISTING_USERS') === true) { const userQuery = { @@ -371,20 +371,20 @@ export function importNewUsers(ldap) { log_debug('userQuery merge', userQuery); - user = Meteor.users.findOne(userQuery); + user = await Meteor.users.findOneAsync(userQuery); if (user) { - syncUserData(user, ldapUser); + await syncUserData(user, ldapUser); } } if (!user) { - addLdapUser(ldapUser, username); + await 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); @@ -394,7 +394,7 @@ export function importNewUsers(ldap) { })); } -function sync() { +async function sync() { if (LDAP.settings_get('LDAP_ENABLE') !== true) { return; } @@ -402,7 +402,7 @@ function sync() { const ldap = new LDAP(); try { - ldap.connectSync(); + await ldap.connect(); let users; if (LDAP.settings_get('LDAP_BACKGROUND_SYNC_KEEP_EXISTANT_USERS_UPDATED') === true) { @@ -410,25 +410,25 @@ function sync() { } if (LDAP.settings_get('LDAP_BACKGROUND_SYNC_IMPORT_NEW_USERS') === true) { - importNewUsers(ldap); + await importNewUsers(ldap); } if (LDAP.settings_get('LDAP_BACKGROUND_SYNC_KEEP_EXISTANT_USERS_UPDATED') === true) { - users.forEach(function(user) { + for await (const user of users) { let ldapUser; if (user.services && user.services.ldap && user.services.ldap.id) { - ldapUser = ldap.getUserByIdSync(user.services.ldap.id, user.services.ldap.idAttribute); + ldapUser = await ldap.getUserById(user.services.ldap.id, user.services.ldap.idAttribute); } else { - ldapUser = ldap.getUserByUsernameSync(user.username); + ldapUser = await ldap.getUserByUsername(user.username); } if (ldapUser) { - syncUserData(user, ldapUser); + await syncUserData(user, ldapUser); } else { log_info('Can\'t sync user', user.username); } - }); + } } } catch (error) { log_error(error); @@ -459,8 +459,8 @@ const addCronJob = _.debounce(Meteor.bindEnvironment(function addCronJobDebounce else { return parser.recur().on(0).minute(); }}, - job: function() { - sync(); + job: async function() { + await sync(); }, }); sc.start(); diff --git a/packages/wekan-ldap/server/syncUser.js b/packages/wekan-ldap/server/syncUser.js index a05ab4f0a..9558bdee1 100644 --- a/packages/wekan-ldap/server/syncUser.js +++ b/packages/wekan-ldap/server/syncUser.js @@ -2,7 +2,7 @@ import {importNewUsers} from './sync'; import LDAP from './ldap'; Meteor.methods({ - ldap_sync_now() { + async ldap_sync_now() { const user = Meteor.user(); if (!user) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'ldap_sync_users' }); @@ -18,7 +18,7 @@ Meteor.methods({ this.unblock(); - importNewUsers(); + await importNewUsers(); return { message: 'Sync_in_progress', diff --git a/packages/wekan-ldap/server/testConnection.js b/packages/wekan-ldap/server/testConnection.js index 02866ce54..254c79fac 100644 --- a/packages/wekan-ldap/server/testConnection.js +++ b/packages/wekan-ldap/server/testConnection.js @@ -1,7 +1,7 @@ import LDAP from './ldap'; Meteor.methods({ - ldap_test_connection() { + async ldap_test_connection() { const user = Meteor.user(); if (!user) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'ldap_test_connection' }); @@ -19,14 +19,14 @@ Meteor.methods({ let ldap; try { ldap = new LDAP(); - ldap.connectSync(); + await ldap.connect(); } catch (error) { console.log(error); throw new Meteor.Error(error.message); } try { - ldap.bindIfNecessary(); + await ldap.bindIfNecessary(); } catch (error) { throw new Meteor.Error(error.name || error.message); } From fb79ccaa55e383e695a7084b64a4e905625dbf28 Mon Sep 17 00:00:00 2001 From: Harry Adel Date: Thu, 29 Jan 2026 21:01:39 +0200 Subject: [PATCH 2/3] Replace slugify with limax and fix sync operations --- packages/wekan-ldap/package.js | 6 +++++- packages/wekan-ldap/server/loginHandler.js | 2 +- packages/wekan-ldap/server/sync.js | 5 +++-- packages/wekan-ldap/server/testConnection.js | 2 +- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/wekan-ldap/package.js b/packages/wekan-ldap/package.js index 5402afde6..97dcc7b74 100644 --- a/packages/wekan-ldap/package.js +++ b/packages/wekan-ldap/package.js @@ -12,7 +12,6 @@ Package.describe({ Package.onUse(function(api) { - api.use('yasaricli:slugify'); api.use('ecmascript'); api.use('underscore'); api.use('sha'); @@ -25,3 +24,8 @@ Package.onUse(function(api) { api.mainModule('server/index.js', 'server'); }); + +Npm.depends({ + 'ldapjs': '2.3.3', + 'limax': '4.1.0' +}); diff --git a/packages/wekan-ldap/server/loginHandler.js b/packages/wekan-ldap/server/loginHandler.js index 8864b0169..29ff386fa 100644 --- a/packages/wekan-ldap/server/loginHandler.js +++ b/packages/wekan-ldap/server/loginHandler.js @@ -203,7 +203,7 @@ Accounts.registerLoginHandler('ldap', async function(loginRequest) { await syncUserData(user, ldapUser); if (LDAP.settings_get('LDAP_LOGIN_FALLBACK') === true) { - Accounts.setPassword(user._id, loginRequest.ldapPass, {logout: false}); + await Accounts.setPasswordAsync(user._id, loginRequest.ldapPass, {logout: false}); } return { diff --git a/packages/wekan-ldap/server/sync.js b/packages/wekan-ldap/server/sync.js index d6da2a34e..7c8e48cf2 100644 --- a/packages/wekan-ldap/server/sync.js +++ b/packages/wekan-ldap/server/sync.js @@ -1,5 +1,6 @@ import _ from 'underscore'; import { SyncedCron } from 'meteor/quave:synced-cron'; +import limax from 'limax'; import LDAP from './ldap'; import { log_debug, log_info, log_warn, log_error } from './logger'; @@ -20,7 +21,7 @@ export function slug(text) { if (LDAP.settings_get('LDAP_UTF8_NAMES_SLUGIFY') !== true) { return text; } - text = slugify(text, '.'); + text = limax(text, { separator: '.' }); return text.replace(/[^0-9a-z-_.]/g, ''); } @@ -307,7 +308,7 @@ export async function addLdapUser(ldapUser, username, password) { try { // This creates the account with password service userObject.ldap = true; - userObject._id = Accounts.createUser(userObject); + userObject._id = await Accounts.createUserAsync(userObject); // Add the services.ldap identifiers await Meteor.users.updateAsync({ _id: userObject._id }, { diff --git a/packages/wekan-ldap/server/testConnection.js b/packages/wekan-ldap/server/testConnection.js index 254c79fac..50ad2638a 100644 --- a/packages/wekan-ldap/server/testConnection.js +++ b/packages/wekan-ldap/server/testConnection.js @@ -12,7 +12,7 @@ Meteor.methods({ // throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'ldap_test_connection' }); //} - if (LDAP.settings_get(LDAP_ENABLE) !== true) { + if (LDAP.settings_get('LDAP_ENABLE') !== true) { throw new Meteor.Error('LDAP_disabled'); } From 217cd7bcb6f61ef1673cbf53180ac6249c315461 Mon Sep 17 00:00:00 2001 From: Harry Adel Date: Fri, 13 Feb 2026 18:45:59 +0200 Subject: [PATCH 3/3] Use ldapts instead ldapjs We're using ldapts v4.2.6 because Wekan is on 2.16 --- .meteor/versions | 3 +- packages/wekan-ldap/package.js | 2 +- packages/wekan-ldap/server/ldap.js | 363 +++++++++-------------------- packages/wekan-ldap/server/sync.js | 72 +++--- 4 files changed, 143 insertions(+), 297 deletions(-) diff --git a/.meteor/versions b/.meteor/versions index fe3bd637a..9ae1e21f4 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -143,8 +143,7 @@ wekan-accounts-oidc@1.0.10 wekan-accounts-sandstorm@0.9.0 wekan-fontawesome@6.4.2 wekan-fullcalendar@3.10.5 -wekan-ldap@0.0.2 +wekan-ldap@0.1.0 wekan-markdown@1.0.9 wekan-oidc@1.1.0 -yasaricli:slugify@0.0.7 zodern:types@1.0.13 diff --git a/packages/wekan-ldap/package.js b/packages/wekan-ldap/package.js index 97dcc7b74..6e7b367c7 100644 --- a/packages/wekan-ldap/package.js +++ b/packages/wekan-ldap/package.js @@ -26,6 +26,6 @@ Package.onUse(function(api) { }); Npm.depends({ - 'ldapjs': '2.3.3', + 'ldapts': '4.2.6', 'limax': '4.1.0' }); diff --git a/packages/wekan-ldap/server/ldap.js b/packages/wekan-ldap/server/ldap.js index 3fd37f5c2..94dfe7020 100644 --- a/packages/wekan-ldap/server/ldap.js +++ b/packages/wekan-ldap/server/ldap.js @@ -1,4 +1,4 @@ -import ldapjs from 'ldapjs'; +import { Client } from 'ldapts'; import { Log } from 'meteor/logging'; // copied from https://github.com/ldapjs/node-ldapjs/blob/a113953e0d91211eb945d2a3952c84b7af6de41c/lib/filters/index.js#L167 @@ -18,10 +18,14 @@ function escapedToHex (str) { } } +// Convert hex string to LDAP escaped binary filter value +// e.g. "0102ff" -> "\\01\\02\\ff" +function hexToLdapEscaped(hex) { + return hex.match(/.{2}/g).map(h => '\\' + h).join(''); +} + export default class LDAP { constructor() { - this.ldapjs = ldapjs; - this.connected = false; this.options = { @@ -74,54 +78,8 @@ export default class LDAP { } async connect() { - return new Promise((resolve, reject) => { - this.connectAsync((error, result) => { - if (error) { - reject(error); - } else { - resolve(result); - } - }); - }); - } - - async searchAll(BaseDN, options) { - return new Promise((resolve, reject) => { - this.searchAllAsync(BaseDN, options, (error, result) => { - if (error) { - reject(error); - } else { - resolve(result); - } - }); - }); - } - - async bind(dn, password) { - return new Promise((resolve, reject) => { - this.client.bind(dn, password, (error) => { - if (error) { - reject(error); - } else { - resolve(); - } - }); - }); - } - - 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, - }; - const tlsOptions = { rejectUnauthorized: this.options.reject_unauthorized, }; @@ -141,79 +99,114 @@ export default class LDAP { tlsOptions.ca = ca; } + let url; if (this.options.encryption === 'ssl') { - connectionOptions.url = `ldaps://${connectionOptions.url}`; - connectionOptions.tlsOptions = tlsOptions; + url = `ldaps://${this.options.host}:${this.options.port}`; } else { - connectionOptions.url = `ldap://${connectionOptions.url}`; + url = `ldap://${this.options.host}:${this.options.port}`; } - Log.info(`Connecting ${connectionOptions.url}`); - Log.debug(`connectionOptions ${JSON.stringify(connectionOptions)}`); + Log.info(`Connecting ${url}`); - this.client = ldapjs.createClient(connectionOptions); + const clientOptions = { + url, + timeout : this.options.timeout, + connectTimeout: this.options.connect_timeout, + strictDN : false, + }; - this.client.on('error', (error) => { - Log.error(`connection ${error}`); - if (replied === false) { - replied = true; - callback(error, null); - } - }); + if (this.options.encryption === 'ssl') { + clientOptions.tlsOptions = tlsOptions; + } - this.client.on('idle', () => { - Log.info('Idle'); - this.disconnect(); - }); + Log.debug(`clientOptions ${JSON.stringify(clientOptions)}`); - this.client.on('close', () => { - Log.info('Closed'); - }); + this.client = new Client(clientOptions); 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). + // Set host parameter for tls.connect which is used by 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 ${JSON.stringify(tlsOptions)}`); - this.client.starttls(tlsOptions, null, (error, response) => { - if (error) { - Log.error(`TLS connection ${JSON.stringify(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); - } - }); + await this.client.startTLS(tlsOptions); + Log.info('TLS connected'); } - setTimeout(() => { - if (replied === false) { - Log.error(`connection time out ${connectionOptions.connectTimeout}`); - replied = true; - callback(new Error('Timeout')); + this.connected = true; + } + + async bind(dn, password) { + await this.client.bind(dn, password); + } + + getBufferAttributes() { + const fields = []; + let uidField = this.constructor.settings_get('LDAP_UNIQUE_IDENTIFIER_FIELD'); + if (uidField && uidField !== '') { + fields.push(...uidField.replace(/\s/g, '').split(',')); + } + let searchField = this.constructor.settings_get('LDAP_USER_SEARCH_FIELD'); + if (searchField && searchField !== '') { + fields.push(...searchField.replace(/\s/g, '').split(',')); + } + return fields; + } + + async searchAll(BaseDN, options) { + const searchOptions = { + filter: options.filter, + scope : options.scope || 'sub', + }; + + if (options.attributes) { + searchOptions.attributes = options.attributes; + } + + if (options.sizeLimit) { + searchOptions.sizeLimit = options.sizeLimit; + } + + if (options.paged) { + searchOptions.paged = { + pageSize: options.paged.pageSize || 250, + }; + } + + // Request unique identifier fields as Buffers so that + // getLdapUserUniqueID() in sync.js can call .toString('hex') + const bufferAttributes = this.getBufferAttributes(); + if (bufferAttributes.length > 0) { + searchOptions.explicitBufferAttributes = bufferAttributes; + } + + const { searchEntries } = await this.client.search(BaseDN, searchOptions); + + Log.info(`Search result count ${searchEntries.length}`); + return searchEntries.map((entry) => this.extractLdapEntryData(entry)); + } + + extractLdapEntryData(entry) { + const values = { + _raw: {}, + }; + + for (const key of Object.keys(entry)) { + const value = entry[key]; + values._raw[key] = value; + + if (!['thumbnailPhoto', 'jpegPhoto'].includes(key)) { + if (value instanceof Buffer) { + values[key] = value.toString(); + } else { + values[key] = value; + } } - }, connectionOptions.connectTimeout); + } + + return values; } getUserFilter(username) { @@ -285,7 +278,7 @@ export default class LDAP { this.domainBinded = true; } - async searchUsers(username, page) { + async searchUsers(username) { await this.bindIfNecessary(); const searchOptions = { filter : this.getUserFilter(username), @@ -297,8 +290,7 @@ export default class LDAP { if (this.options.Search_Page_Size > 0) { searchOptions.paged = { - pageSize : this.options.Search_Page_Size, - pagePause: !!page, + pageSize: this.options.Search_Page_Size, }; } @@ -306,10 +298,6 @@ export default class LDAP { Log.debug(`searchOptions ${searchOptions}`); Log.debug(`BaseDN ${this.options.BaseDN}`); - if (page) { - return this.searchAllPaged(this.options.BaseDN, searchOptions, page); - } - return await this.searchAll(this.options.BaseDN, searchOptions); } @@ -318,23 +306,14 @@ export default class LDAP { const Unique_Identifier_Field = this.constructor.settings_get('LDAP_UNIQUE_IDENTIFIER_FIELD').split(','); + const escapedValue = hexToLdapEscaped(id); let filter; if (attribute) { - filter = new this.ldapjs.filters.EqualityFilter({ - attribute, - value: Buffer.from(id, 'hex'), - }); + filter = `(${attribute}=${escapedValue})`; } 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 filters = Unique_Identifier_Field.map((item) => `(${item}=${escapedValue})`); + filter = `(|${filters.join('')})`; } const searchOptions = { @@ -343,7 +322,7 @@ export default class LDAP { }; Log.info(`Searching by id ${id}`); - Log.debug(`search filter ${searchOptions.filter.toString()}`); + Log.debug(`search filter ${searchOptions.filter}`); Log.debug(`BaseDN ${this.options.BaseDN}`); const result = await this.searchAll(this.options.BaseDN, searchOptions); @@ -471,134 +450,6 @@ export default class LDAP { 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); - }); - }); - } - async auth(dn, password) { Log.info(`Authenticating ${dn}`); @@ -616,10 +467,14 @@ export default class LDAP { } } - disconnect() { + async disconnect() { this.connected = false; this.domainBinded = false; Log.info('Disconecting'); - this.client.unbind(); + try { + await this.client.unbind(); + } catch (error) { + Log.debug('Error during disconnect', error); + } } } diff --git a/packages/wekan-ldap/server/sync.js b/packages/wekan-ldap/server/sync.js index 7c8e48cf2..802fcebf7 100644 --- a/packages/wekan-ldap/server/sync.js +++ b/packages/wekan-ldap/server/sync.js @@ -341,58 +341,50 @@ export async function importNewUsers(ldap) { } let count = 0; - ldap.searchUsers('*', Meteor.bindEnvironment(async (error, ldapUsers, {next, end} = {}) => { - if (error) { - throw error; + const ldapUsers = await ldap.searchUsers('*'); + + for (const ldapUser of ldapUsers) { + 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)); } - for (const ldapUser of ldapUsers) { - count++; + // Add user if it was not added before + let user = await Meteor.users.findOneAsync(userQuery); - const uniqueId = getLdapUserUniqueID(ldapUser); - // Look to see if user already exists + if (!user && username && LDAP.settings_get('LDAP_MERGE_EXISTING_USERS') === true) { const userQuery = { - 'services.ldap.id': uniqueId.value, + username, }; - log_debug('userQuery', userQuery); + log_debug('userQuery merge', userQuery); - let username; - if (LDAP.settings_get('LDAP_USERNAME_FIELD') !== '') { - username = slug(getLdapUsername(ldapUser)); - } - - // Add user if it was not added before - let user = await Meteor.users.findOneAsync(userQuery); - - if (!user && username && LDAP.settings_get('LDAP_MERGE_EXISTING_USERS') === true) { - const userQuery = { - username, - }; - - log_debug('userQuery merge', userQuery); - - user = await Meteor.users.findOneAsync(userQuery); - if (user) { - await syncUserData(user, ldapUser); - } - } - - if (!user) { - await addLdapUser(ldapUser, username); - } - - if (count % 100 === 0) { - log_info('Import running. Users imported until now:', count); + user = await Meteor.users.findOneAsync(userQuery); + if (user) { + await syncUserData(user, ldapUser); } } - if (end) { - log_info('Import finished. Users imported:', count); + if (!user) { + await addLdapUser(ldapUser, username); } - next(count); - })); + if (count % 100 === 0) { + log_info('Import running. Users imported until now:', count); + } + } + + log_info('Import finished. Users imported:', count); } async function sync() {