const app = require('vn-loopback/server/server'); const ldap = require('../util/ldapjs-extra'); const crypto = require('crypto'); const nthash = require('smbhash').nthash; module.exports = Self => { Self.getSynchronizer = 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 }); await this.client.bind(this.rdn, this.password); }, async deinit() { await this.client.unbind(); }, async syncUser(userName, info, password) { let { client, accountConfig } = this; let newEntry; if (info.hasAccount) { let {user} = info; 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 { await client.del(dn); operation = 'delete'; } catch (e) { if (e.name !== 'NoSuchObjectError') throw e; } if (info.hasAccount) { await client.add(dn, newEntry); operation = 'add'; } if (operation === 'delete') console.log(` -> User '${userName}' removed from LDAP`); }, async syncUserGroups(userName, info) { let {client} = this; let opts = { scope: 'sub', attributes: ['dn'], filter: `&(memberUid=${userName})(objectClass=posixGroup)` }; let oldGroups = await client.searchAll(this.groupDn, opts); 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},${this.groupDn}`; reqs.push(client.modify(dn, change)); } await Promise.all(reqs); }, 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 $.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}; }); // 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 = { 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}`; 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; }