const SyncConnector = require('./sync-connector'); const nthash = require('smbhash').nthash; const ldap = require('./ldapjs-extra'); const crypto = require('crypto'); class SyncLdap extends SyncConnector { async init() { let ldapConfig = await this.$.LdapConfig.findOne({ fields: [ 'server', 'rdn', 'password', 'userDn', 'groupDn' ] }); if (!ldapConfig) return false; let client = ldap.createClient({ url: ldapConfig.server }); await client.bind(ldapConfig.rdn, ldapConfig.password); Object.assign(this, { ldapConfig, client }); return true; } async deinit() { if (this.client) await this.client.unbind(); } async sync(info, userName, password) { let { ldapConfig, client, accountConfig } = this; let {user} = info; let res = await client.search(ldapConfig.userDn, { scope: 'sub', attributes: ['userPassword', 'sambaNTPassword'], filter: `&(uid=${userName})` }); 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.userDn}`; await client.del(dn); } catch (e) { if (e.name !== 'NoSuchObjectError') throw e; } if (!info.hasAccount) { if (oldUser) console.log(` -> '${userName}' removed from LDAP`); return; } let nickname = user.nickname || userName; let nameArgs = nickname.trim().split(' '); let sn = nameArgs.length > 1 ? nameArgs.splice(1).join(' ') : '-'; let dn = `uid=${userName},${ldapConfig.userDn}`; let newEntry = { uid: userName, objectClass: [ 'inetOrgPerson', 'posixAccount', 'sambaSamAccount' ], cn: nickname, displayName: nickname, givenName: nameArgs[0], sn, mail: info.corporateMail, preferredLanguage: user.lang, 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); } async syncGroups(info, userName) { let { ldapConfig, client } = this; let res = await client.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 (let oldGroup of oldGroups) { let change = new ldap.Change({ operation: 'delete', modification: {memberUid: userName} }); reqs.push(client.modify(oldGroup.dn, change)); } await Promise.all(reqs); if (!info.hasAccount) return; reqs = []; for (let role of info.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(client.modify(dn, change)); } await Promise.all(reqs); } async getUsers(usersToSync) { let { ldapConfig, client } = this; let res = await client.search(ldapConfig.userDn, { 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); }); } async syncRoles() { let { $, ldapConfig, client, accountConfig } = this; // Delete roles let opts = { scope: 'sub', attributes: ['dn'], filter: 'objectClass=posixGroup' }; let res = await client.search(ldapConfig.groupDn, opts); let reqs = []; await new Promise((resolve, reject) => { res.on('error', err => { if (err.name === 'NoSuchObjectError') err = new Error(`Object '${ldapConfig.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 let roles = await $.Role.find({ fields: ['id', 'name', 'description'] }); let roleRoles = await $.RoleRole.find({ fields: ['role', 'inheritsFrom'] }); let roleMap = toMap(roleRoles, e => { return {key: e.inheritsFrom, val: e.role}; }); let accounts = await $.UserAccount.find({ fields: ['id'], include: { relation: 'user', scope: { fields: ['name', 'roleFk'], where: {active: true} } } }); let accountMap = toMap(accounts, e => { let user = e.user(); if (!user) return; return {key: user.roleFk, val: user.name}; }); reqs = []; for (let role of roles) { let newEntry = { objectClass: ['top', 'posixGroup'], cn: role.name, description: role.description, gidNumber: accountConfig.idBase + role.id }; let memberUid = []; for (let subrole of roleMap.get(role.id) || []) memberUid = memberUid.concat(accountMap.get(subrole) || []); if (memberUid.length) { memberUid.sort((a, b) => a.localeCompare(b)); newEntry.memberUid = memberUid; } let dn = `cn=${role.name},${ldapConfig.groupDn}`; reqs.push(client.add(dn, newEntry)); } await Promise.all(reqs); } } SyncConnector.connectors.push(SyncLdap); module.exports = SyncLdap; 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; }