const ldap = require('../util/ldapjs-extra'); const execFile = require('child_process').execFile; /** * Summary of userAccountControl flags: * https://docs.microsoft.com/en-us/troubleshoot/windows-server/identity/useraccountcontrol-manipulate-account-properties */ const UserAccountControlFlags = { ACCOUNTDISABLE: 0x2, DONT_EXPIRE_PASSWD: 0x10000 }; module.exports = Self => { const shouldSync = process.env.NODE_ENV !== 'test'; Self.getLinker = async function() { return await Self.findOne({ fields: [ 'host', 'adDomain', 'adController', 'adUser', 'adPassword', 'userDn', 'verifyCert' ] }); }; Object.assign(Self.prototype, { async init() { const baseDn = this.adDomain .split('.') .map(part => `dc=${part}`) .join(','); const bindDn = `cn=${this.adUser},cn=Users,${baseDn}`; const adClient = ldap.createClient({ url: `ldaps://${this.adController}:636`, tlsOptions: {rejectUnauthorized: this.verifyCert} }); adClient.on('error', () => {}); await adClient.bind(bindDn, this.adPassword); Object.assign(this, { adClient, fullUsersDn: `${this.userDn},${baseDn}`, bindDn }); }, async deinit() { await this.adClient.unbind(); }, async sambaTool(command, args = []) { let authArgs = [ '--URL', `ldaps://${this.adController}`, '--simple-bind-dn', this.bindDn, '--password', this.adPassword ]; if (!this.verifyCert) authArgs.push('--option', 'tls verify peer = no_check'); const allArgs = [command].concat( args, authArgs ); if (!shouldSync) return; return await new Promise((resolve, reject) => { execFile('samba-tool', allArgs, (err, stdout, stderr) => { if (err) reject(err); else resolve({stdout, stderr}); }); }); }, async getAdUser(userName) { const sambaUser = await this.adClient.searchOne(this.fullUsersDn, { scope: 'sub', attributes: [ 'dn', 'userAccountControl', 'uidNumber', 'accountExpires', 'mail' ], filter: `(&(objectClass=user)(sAMAccountName=${userName}))` }); if (sambaUser) { for (const intProp of ['uidNumber', 'userAccountControl']) { if (sambaUser[intProp] != null) sambaUser[intProp] = parseInt(sambaUser[intProp]); } } return sambaUser; }, async syncUser(userName, info, password) { let sambaUser = await this.getAdUser(userName); let entry; if (info.hasAccount) { if (!sambaUser) { await this.sambaTool('user', [ 'create', userName, '--userou', this.userDn, '--random-password' ]); sambaUser = await this.getAdUser(userName); } if (password) { await this.sambaTool('user', [ 'setpassword', userName, '--newpassword', password ]); } entry = { userAccountControl: (sambaUser.userAccountControl | UserAccountControlFlags.DONT_EXPIRE_PASSWD) & ~UserAccountControlFlags.ACCOUNTDISABLE, uidNumber: info.uidNumber, accountExpires: 0, mail: info.corporateMail }; } else if (sambaUser) { entry = { userAccountControl: sambaUser.userAccountControl | UserAccountControlFlags.ACCOUNTDISABLE }; // eslint-disable-next-line no-console console.log(` -> User '${userName}' disabled on Samba`); } if (sambaUser && entry) { const changes = []; for (const prop in entry) { if (sambaUser[prop] == entry[prop]) continue; changes.push(new ldap.Change({ operation: 'replace', modification: { [prop]: entry[prop] } })); } if (changes.length && shouldSync) await this.adClient.modify(sambaUser.dn, changes); } }, /** * Gets Samba enabled users. * * @param {Set} usersToSync */ async getUsers(usersToSync) { const LDAP_MATCHING_RULE_BIT_AND = '1.2.840.113556.1.4.803'; const filter = `!(userAccountControl:${LDAP_MATCHING_RULE_BIT_AND}` + `:=${UserAccountControlFlags.ACCOUNTDISABLE})`; const opts = { scope: 'sub', attributes: ['sAMAccountName'], filter: `(&(objectClass=user)(${filter}))` }; await this.adClient.searchForeach(this.fullUsersDn, opts, o => usersToSync.add(o.sAMAccountName)); } }); };