From 6e5eac1f7837d68d95d6e47476ca065a9c2a9a48 Mon Sep 17 00:00:00 2001 From: Juan Ferrer Toribio Date: Fri, 13 Nov 2020 13:20:37 +0100 Subject: [PATCH] Account synchronization fixes --- modules/account/back/models/account-config.js | 8 +- modules/account/back/models/ldap-config.js | 226 +++++++++--------- modules/account/back/models/samba-config.js | 66 ++--- modules/account/back/util/ldapjs-extra.js | 31 +++ 4 files changed, 178 insertions(+), 153 deletions(-) diff --git a/modules/account/back/models/account-config.js b/modules/account/back/models/account-config.js index a9b14bb4a..83fe3332f 100644 --- a/modules/account/back/models/account-config.js +++ b/modules/account/back/models/account-config.js @@ -20,7 +20,7 @@ module.exports = Self => { async syncUsers() { let instance = await Self.getInstance(); - let usersToSync = instance.getUsers(); + let usersToSync = await instance.synchronizerGetUsers(); usersToSync = Array.from(usersToSync.values()) .sort((a, b) => a.localeCompare(b)); @@ -28,9 +28,9 @@ module.exports = Self => { try { console.log(`Synchronizing user '${userName}'`); await instance.synchronizerSyncUser(userName); - console.log(` -> '${userName}' sinchronized`); + console.log(` -> User '${userName}' sinchronized`); } catch (err) { - console.error(` -> '${userName}' synchronization error:`, err.message); + console.error(` -> User '${userName}' synchronization error:`, err.message); } } @@ -171,7 +171,7 @@ module.exports = Self => { }, async syncUser(userName, info, password) { - if (info.user) + if (info.user && password) await app.models.user.setPassword(info.user.id, password); }, diff --git a/modules/account/back/models/ldap-config.js b/modules/account/back/models/ldap-config.js index 3a121aa42..9f0e84c66 100644 --- a/modules/account/back/models/ldap-config.js +++ b/modules/account/back/models/ldap-config.js @@ -35,109 +35,104 @@ module.exports = Self => { accountConfig } = this; - let {user} = info; + let newEntry; - let res = await client.search(this.userDn, { - scope: 'sub', - attributes: ['userPassword', 'sambaNTPassword'], - filter: `&(uid=${userName})` - }); + if (info.hasAccount) { + let {user} = info; - let oldUser; - await new Promise((resolve, reject) => { - res.on('error', reject); - res.on('searchEntry', e => oldUser = e.object); - res.on('end', resolve); - }); + let oldUser = await client.searchOne(this.userDn, { + scope: 'sub', + attributes: ['userPassword', 'sambaNTPassword'], + filter: `&(uid=${userName})` + }); + + let nickname = user.nickname || userName; + let nameArgs = nickname.trim().split(' '); + let sn = nameArgs.length > 1 + ? nameArgs.splice(1).join(' ') + : '-'; + + newEntry = { + uid: userName, + objectClass: [ + 'inetOrgPerson', + 'posixAccount', + 'sambaSamAccount' + ], + cn: nickname, + displayName: nickname, + givenName: nameArgs[0], + sn, + mail: info.corporateMail, + preferredLanguage: user.lang || 'en', + homeDirectory: `${accountConfig.homedir}/${userName}`, + loginShell: accountConfig.shell, + uidNumber: info.uidNumber, + gidNumber: accountConfig.idBase + user.roleFk, + sambaSID: '-' + }; + + if (password) { + let salt = crypto + .randomBytes(8) + .toString('base64'); + + let hash = crypto.createHash('sha1'); + hash.update(password); + hash.update(salt, 'binary'); + let digest = hash.digest('binary'); + + let ssha = Buffer + .from(digest + salt, 'binary') + .toString('base64'); + + Object.assign(newEntry, { + userPassword: `{SSHA}${ssha}`, + sambaNTPassword: nthash(password) + }); + } else if (oldUser) { + Object.assign(newEntry, { + userPassword: oldUser.userPassword, + sambaNTPassword: oldUser.sambaNTPassword + }); + } + + for (let prop in newEntry) { + if (newEntry[prop] == null) + delete newEntry[prop]; + } + } + + // Remove and recreate (if applicable) user + + let dn = `uid=${userName},${this.userDn}`; + let operation; try { - let dn = `uid=${userName},${this.userDn}`; await client.del(dn); + operation = 'delete'; } catch (e) { if (e.name !== 'NoSuchObjectError') throw e; } - if (!info.hasAccount) { - if (oldUser) - console.log(` -> '${userName}' removed from LDAP`); - return; + if (info.hasAccount) { + await client.add(dn, newEntry); + operation = 'add'; } - let nickname = user.nickname || userName; - let nameArgs = nickname.trim().split(' '); - let sn = nameArgs.length > 1 - ? nameArgs.splice(1).join(' ') - : '-'; - - let dn = `uid=${userName},${this.userDn}`; - let newEntry = { - uid: userName, - objectClass: [ - 'inetOrgPerson', - 'posixAccount', - 'sambaSamAccount' - ], - cn: nickname, - displayName: nickname, - givenName: nameArgs[0], - sn, - mail: info.corporateMail, - preferredLanguage: user.lang || 'en', - homeDirectory: `${accountConfig.homedir}/${userName}`, - loginShell: accountConfig.shell, - uidNumber: info.uidNumber, - gidNumber: accountConfig.idBase + user.roleFk, - sambaSID: '-' - }; - - if (password) { - let salt = crypto - .randomBytes(8) - .toString('base64'); - - let hash = crypto.createHash('sha1'); - hash.update(password); - hash.update(salt, 'binary'); - let digest = hash.digest('binary'); - - let ssha = Buffer - .from(digest + salt, 'binary') - .toString('base64'); - - Object.assign(newEntry, { - userPassword: `{SSHA}${ssha}`, - sambaNTPassword: nthash(password) - }); - } else if (oldUser) { - Object.assign(newEntry, { - userPassword: oldUser.userPassword, - sambaNTPassword: oldUser.sambaNTPassword - }); - } - - for (let prop in newEntry) { - if (newEntry[prop] == null) - delete newEntry[prop]; - } - - await client.add(dn, newEntry); + if (operation === 'delete') + console.log(` -> User '${userName}' removed from LDAP`); }, async syncUserGroups(userName, info) { let {client} = this; - let res = await client.search(this.groupDn, { + let opts = { scope: 'sub', attributes: ['dn'], filter: `&(memberUid=${userName})(objectClass=posixGroup)` - }); - - let oldGroups = []; - await new Promise((resolve, reject) => { - res.on('error', reject); - res.on('searchEntry', e => oldGroups.push(e.object)); - res.on('end', resolve); - }); + }; + let oldGroups = await client.searchAll(this.groupDn, opts); let reqs = []; for (let oldGroup of oldGroups) { @@ -167,17 +162,13 @@ module.exports = Self => { async getUsers(usersToSync) { let {client} = this; - let res = await client.search(this.userDn, { + let opts = { scope: 'sub', attributes: ['uid'], filter: `uid=*` - }); - - await new Promise((resolve, reject) => { - res.on('error', reject); - res.on('searchEntry', e => usersToSync.add(e.object.uid)); - res.on('end', resolve); - }); + }; + await client.searchForeach(this.userDn, opts, + o => usersToSync.add(o.uid)); }, async syncRoles() { @@ -187,30 +178,7 @@ module.exports = Self => { accountConfig } = this; - // Delete roles - - let opts = { - scope: 'sub', - attributes: ['dn'], - filter: 'objectClass=posixGroup' - }; - let res = await client.search(this.groupDn, opts); - - let reqs = []; - await new Promise((resolve, reject) => { - res.on('error', err => { - if (err.name === 'NoSuchObjectError') - err = new Error(`Object '${this.groupDn}' does not exist`); - reject(err); - }); - res.on('searchEntry', e => { - reqs.push(client.del(e.object.dn)); - }); - res.on('end', resolve); - }); - await Promise.all(reqs); - - // Recreate roles + // Prepare data let roles = await $.Role.find({ fields: ['id', 'name', 'description'] @@ -238,6 +206,20 @@ module.exports = Self => { return {key: user.roleFk, val: user.name}; }); + // Delete roles + + let opts = { + scope: 'sub', + attributes: ['dn'], + filter: 'objectClass=posixGroup' + }; + let reqs = []; + await client.searchForeach(this.groupDn, opts, + o => reqs.push(client.del(o.dn))); + await Promise.all(reqs); + + // Recreate roles + reqs = []; for (let role of roles) { let newEntry = { @@ -263,3 +245,15 @@ module.exports = Self => { } }); }; + +function toMap(array, fn) { + let map = new Map(); + for (let item of array) { + let keyVal = fn(item); + if (!keyVal) continue; + let key = keyVal.key; + if (!map.has(key)) map.set(key, []); + map.get(key).push(keyVal.val); + } + return map; +} diff --git a/modules/account/back/models/samba-config.js b/modules/account/back/models/samba-config.js index 02477a5f3..5fd62a68b 100644 --- a/modules/account/back/models/samba-config.js +++ b/modules/account/back/models/samba-config.js @@ -2,6 +2,14 @@ const ldap = require('../util/ldapjs-extra'); const ssh = require('node-ssh'); +/** + * Summary of userAccountControl flags: + * https://docs.microsoft.com/en-us/troubleshoot/windows-server/identity/useraccountcontrol-manipulate-account-properties + */ +const UserAccountControlFlags = { + ACCOUNTDISABLE: 2 +}; + module.exports = Self => { Self.getSynchronizer = async function() { return await Self.findOne({ @@ -55,8 +63,16 @@ module.exports = Self => { async syncUser(userName, info, password) { let {sshClient} = this; + let sambaUser = await this.adClient.searchOne(this.usersDn(), { + scope: 'sub', + attributes: ['userAccountControl'], + filter: `(&(objectClass=user)(sAMAccountName=${userName}))` + }); + let isEnabled = sambaUser + && !(sambaUser.userAccountControl & UserAccountControlFlags.ACCOUNTDISABLE); + if (info.hasAccount) { - try { + if (!sambaUser) { await sshClient.exec('samba-tool user create', [ userName, '--uid-number', `${info.uidNumber}`, @@ -71,58 +87,42 @@ module.exports = Self => { userName, '0027' ]); - } catch (e) {} - - await sshClient.exec('samba-tool user enable', [ - userName - ]); - + } + if (!isEnabled) { + await sshClient.exec('samba-tool user enable', [ + userName + ]); + } if (password) { await sshClient.exec('samba-tool user setpassword', [ userName, '--newpassword', password ]); } - } else { - try { - await sshClient.exec('samba-tool user disable', [ - userName - ]); - console.log(` -> '${userName}' disabled on Samba`); - } catch (e) {} + } else if (isEnabled) { + await sshClient.exec('samba-tool user disable', [ + userName + ]); + console.log(` -> User '${userName}' disabled on Samba`); } }, /** * Gets Samba enabled users. * - * Summary of userAccountControl flags: - * https://docs.microsoft.com/en-us/troubleshoot/windows-server/identity/useraccountcontrol-manipulate-account-properties - * * @param {Set} usersToSync */ async getUsers(usersToSync) { - let {adClient} = this; - let usersDn = this.usersDn(); + const LDAP_MATCHING_RULE_BIT_AND = '1.2.840.113556.1.4.803'; + let filter = `!(userAccountControl:${LDAP_MATCHING_RULE_BIT_AND}:=${UserAccountControlFlags.ACCOUNTDISABLE})`; let opts = { scope: 'sub', attributes: ['sAMAccountName'], - filter: '(&(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))' + filter: `(&(objectClass=user)(${filter}))` }; - let res = await adClient.search(usersDn, opts); - - await new Promise((resolve, reject) => { - res.on('error', err => { - if (err.name === 'NoSuchObjectError') - err = new Error(`Object '${usersDn}' does not exist`); - reject(err); - }); - res.on('searchEntry', e => { - usersToSync.add(e.object.sAMAccountName); - }); - res.on('end', resolve); - }); + await this.adClient.searchForeach(this.usersDn(), opts, + o => usersToSync.add(o.sAMAccountName)); } }); }; diff --git a/modules/account/back/util/ldapjs-extra.js b/modules/account/back/util/ldapjs-extra.js index 381eebb6f..b77440a77 100644 --- a/modules/account/back/util/ldapjs-extra.js +++ b/modules/account/back/util/ldapjs-extra.js @@ -26,5 +26,36 @@ function createClient(opts) { 'starttls', 'unbind' ]); + + Object.assign(client, { + async searchForeach(base, options, eachFn, controls) { + let res = await this.search(base, options); + + await new Promise((resolve, reject) => { + res.on('error', err => { + if (err.name === 'NoSuchObjectError') + err = new Error(`Object '${base}' does not exist`); + reject(err); + }); + res.on('searchEntry', e => eachFn(e.object)); + res.on('end', resolve); + }); + }, + + async searchAll(base, options, controls) { + let elements = []; + await this.searchForeach(base, options, + o => elements.push(o), controls); + return elements; + }, + + async searchOne(base, options, controls) { + let object; + await this.searchForeach(base, options, + o => object = o, controls); + return object; + } + }); + return client; }