const ldap = require('../../util/ldapjs-extra'); const nthash = require('smbhash').nthash; const ssh = require('node-ssh'); const crypto = require('crypto'); module.exports = Self => { Self.remoteMethod('sync', { description: 'Synchronizes the user with the other user databases', accepts: [ { arg: 'userName', type: 'string', description: 'The user name', required: true }, { arg: 'password', type: 'string', description: 'The password' } ], http: { path: `/:userName/sync`, verb: 'PATCH' } }); Self.sync = async function(userName, password) { let $ = Self.app.models; let user = await $.Account.findOne({ fields: ['id'], where: {name: userName} }); let isSync = !await $.UserSync.exists(userName); if (user && isSync) return; let sync = await Self.syncInit(); try { await Self.doSync(sync, userName, password); } finally { await Self.syncDeinit(sync); } }; Self.syncInit = async function() { let $ = Self.app.models; let accountConfig = await $.AccountConfig.findOne({ fields: ['homedir', 'shell', 'idBase'] }); let mailConfig = await $.MailConfig.findOne({ fields: ['domain'] }); // LDAP let ldapClient; let ldapConfig = await $.LdapConfig.findOne({ fields: ['host', 'rdn', 'password', 'baseDn', 'groupDn'] }); if (ldapConfig) { ldapClient = ldap.createClient({ url: `ldap://${ldapConfig.host}:389` }); let ldapPassword = Buffer .from(ldapConfig.password, 'base64') .toString('ascii'); await ldapClient.bind(ldapConfig.rdn, ldapPassword); } // Samba let sambaClient; let sambaConfig = await $.SambaConfig.findOne({ fields: ['host', 'sshUser', 'sshPass'] }); if (sambaConfig) { let sshPassword = Buffer .from(sambaConfig.sshPass, 'base64') .toString('ascii'); sambaClient = new ssh.NodeSSH(); await sambaClient.connect({ host: sambaConfig.host, username: sambaConfig.sshUser, password: sshPassword }); } return { accountConfig, mailConfig, ldapConfig, ldapClient, sambaClient }; }; Self.syncDeinit = async function(sync) { // FIXME: Cannot disconnect, hangs on undind() call // if (sync.ldapClient) // await sync.ldapClient.unbind(); if (sync.sambaClient) await sync.sambaClient.dispose(); }; Self.doSync = async function(sync, userName, password) { let $ = Self.app.models; let { accountConfig, mailConfig, ldapConfig, ldapClient, sambaClient } = sync; // Skip conflicting users if (!userName || ['administrator', 'root'].indexOf(userName.toLowerCase()) >= 0) return; let user = await $.Account.findOne({ where: {name: userName}, fields: [ 'id', 'nickname', 'email', 'lang', 'roleFk', 'sync', 'active', 'created', 'bcryptPassword', 'updated' ], include: { relation: 'roles', scope: { include: { relation: 'inherits', scope: { fields: ['name'] } } } } }); let extraParams; let hasAccount = false; if (user) { extraParams = { corporateMail: `${userName}@${mailConfig.domain}`, uidNumber: accountConfig.idBase + user.id }; hasAccount = user.active && await $.UserAccount.exists(user.id); } // Database if (user && user.active) { let bcryptPassword = password ? $.User.hashPassword(password) : user.bcryptPassword; await $.Account.upsertWithWhere({id: user.id}, {bcryptPassword} ); let appUser = { id: user.id, username: userName, email: user.email, created: user.created, updated: user.updated }; if (bcryptPassword) appUser.password = bcryptPassword; if (await $.user.exists(user.id)) await $.user.replaceById(user.id, appUser); else if (bcryptPassword) await $.user.upsert(appUser); } else await $.user.destroyAll({username: userName}); // SIP if (hasAccount && password) { await Self.rawSql('CALL pbx.sip_setPassword(?, ?)', [user.id, password] ); } // LDAP if (ldapClient) { // Deletes user res = await ldapClient.search(ldapConfig.baseDn, { scope: 'sub', attributes: ['userPassword', 'sambaNTPassword'], filter: `&(uid=${userName})(objectClass=inetOrgPerson)` }); let oldUser; await new Promise((resolve, reject) => { res.on('error', reject); res.on('searchEntry', e => oldUser = e.object); res.on('end', resolve); }); try { let dn = `uid=${userName},${ldapConfig.baseDn}`; await ldapClient.del(dn); } catch (e) { if (e.name !== 'NoSuchObjectError') throw e; } // Removes user from groups res = await ldapClient.search(ldapConfig.groupDn, { 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 reqs = []; for (oldGroup of oldGroups) { let change = new ldap.Change({ operation: 'delete', modification: {memberUid: userName} }); reqs.push(ldapClient.modify(oldGroup.dn, change)); } await Promise.all(reqs); if (hasAccount) { // Recreates user let nickname = user.nickname || userName; let nameArgs = nickname.split(' '); let sn = nameArgs.length > 1 ? nameArgs.splice(1).join(' ') : '-'; let dn = `uid=${userName},${ldapConfig.baseDn}`; let newEntry = { uid: userName, objectClass: [ 'inetOrgPerson', 'posixAccount', 'sambaSamAccount' ], cn: nickname, displayName: nickname, givenName: nameArgs[0], sn, mail: extraParams.corporateMail, preferredLanguage: user.lang, homeDirectory: `${accountConfig.homedir}/${userName}`, loginShell: accountConfig.shell, uidNumber: extraParams.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 ldapClient.add(dn, newEntry); // Adds user to groups let reqs = []; for (let role of user.roles()) { let change = new ldap.Change({ operation: 'add', modification: {memberUid: userName} }); let roleName = role.inherits().name; let dn = `cn=${roleName},${ldapConfig.groupDn}`; reqs.push(ldapClient.modify(dn, change)); } await Promise.all(reqs); } } // Samba if (sambaClient) { if (hasAccount) { try { await sambaClient.exec('samba-tool user create', [ userName, '--uid-number', `${extraParams.uidNumber}`, '--mail-address', extraParams.corporateMail, '--random-password' ]); } catch (e) {} await sambaClient.exec('samba-tool user setexpiry', [ userName, '--noexpiry' ]); if (password) { await sambaClient.exec('samba-tool user setpassword', [ userName, '--newpassword', password ]); await sambaClient.exec('samba-tool user enable', [ userName ]); } await sambaClient.exec('mkhomedir_helper', [ userName, '0027' ]); } else { try { await sambaClient.exec('samba-tool user disable', [ userName ]); } catch (e) {} } } // Mark as synchronized await $.UserSync.destroyById(userName); }; };