const app = require('vn-loopback/server/server'); const ldap = require('../util/ldapjs-extra'); const crypto = require('crypto'); const nthash = require('smbhash').nthash; const isProduction = require('vn-loopback/common/methods/application/isProduction'); module.exports = Self => { const shouldSync = isProduction(); Self.getLinker = async function() { return await Self.findOne({ fields: [ 'server', 'rdn', 'password', 'userDn', 'groupDn' ] }); }; Object.assign(Self.prototype, { async init() { this.client = ldap.createClient({ url: this.server }); this.client.on('error', () => {}); await this.client.bind(this.rdn, this.password); }, async deinit() { await this.client.unbind(); }, async syncUser(userName, info, password) { let { client, accountConfig } = this; let dn = `uid=${userName},${this.userDn}`; if (info.hasAccount) { let {user} = info; let oldUser = await client.searchOne(this.userDn, { scope: 'sub', filter: `&(uid=${userName})` }); let nickname = user.nickname || userName; let nameArgs = nickname.trim().split(' '); let sn = nameArgs.length > 1 ? nameArgs.splice(1).join(' ') : '-'; 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]; } if (oldUser) { let changes = []; let skipProps = new Set([ 'dn', 'controls' ]); for (let prop in oldUser) { let deleteProp = !skipProps.has(prop) && !newEntry.hasOwnProperty(prop); if (!deleteProp) continue; changes.push(new ldap.Change({ operation: 'delete', modification: { [prop]: oldUser[prop] } })); } for (let prop in newEntry) { if (this.isEqual(oldUser[prop], newEntry[prop])) continue; changes.push(new ldap.Change({ operation: 'replace', modification: { [prop]: newEntry[prop] } })); } if (shouldSync && changes.length) await client.modify(dn, changes); } else if (shouldSync) await client.add(dn, newEntry); } else { try { if (shouldSync) await client.del(dn); // eslint-disable-next-line no-console console.log(` -> User '${userName}' removed from LDAP`); } catch (e) { if (e.name !== 'NoSuchObjectError') throw e; } } }, isEqual(a, b) { if (Array.isArray(a) && Array.isArray(b)) { if (a.length !== b.length) return false; for (let element of a) { if (b.indexOf(element) === -1) return false; } return true; } else return a == b; }, async syncUserGroups(userName, info) { let {client} = this; let {user} = info; let groupDn = this.groupDn; let opts = { scope: 'sub', attributes: ['dn', 'cn'], filter: `&(memberUid=${userName})(objectClass=posixGroup)` }; let oldGroups = await client.searchAll(groupDn, opts); let deleteGroups = []; let addGroups = []; if (info.hasAccount) { let oldSet = new Set(); oldGroups.forEach(e => oldSet.add(e.cn)); let newSet = new Set(); user.roles().forEach(e => newSet.add(e.inherits().name)); for (let group of oldGroups) { if (!newSet.has(group.cn)) deleteGroups.push(group.cn); } for (let role of user.roles()) { if (!oldSet.has(role.inherits().name)) addGroups.push(role.inherits().name); } } else { for (let group of oldGroups) deleteGroups.push(group.cn); } async function applyOperations(groups, operation) { for (let group of groups) { try { let dn = `cn=${group},${groupDn}`; if (shouldSync) { await client.modify(dn, new ldap.Change({ operation, modification: {memberUid: userName} })); } } catch (err) { if (err.name !== 'NoSuchObjectError') throw err; } } } await applyOperations(deleteGroups, 'delete'); await applyOperations(addGroups, 'add'); }, async getUsers(usersToSync) { let {client} = this; let opts = { scope: 'sub', attributes: ['uid'], filter: `uid=*` }; await client.searchForeach(this.userDn, opts, o => usersToSync.add(o.uid)); }, async syncRoles() { let $ = app.models; let { client, accountConfig } = this; // Prepare data let roles = await $.VnRole.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 $.Account.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}; }); // Delete roles let opts = { scope: 'sub', attributes: ['dn'], filter: 'objectClass=posixGroup' }; let reqs = []; await client.searchForeach(this.groupDn, opts, object => { if (shouldSync) reqs.push(client.del(object.dn)); }); await Promise.all(reqs); // Recreate roles 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},${this.groupDn}`; if (shouldSync) reqs.push(client.add(dn, newEntry)); } await Promise.all(reqs); } }); }; 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; }