171 lines
5.6 KiB
JavaScript
171 lines
5.6 KiB
JavaScript
|
|
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: 2
|
|
};
|
|
|
|
module.exports = Self => {
|
|
const shouldSync = process.env.NODE_ENV !== 'test';
|
|
|
|
Self.getSynchronizer = 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}
|
|
});
|
|
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.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));
|
|
}
|
|
});
|
|
};
|