Merge branch 'test' into dev

This commit is contained in:
Juan Ferrer 2020-11-13 13:50:15 +01:00
commit dde89e3b9f
4 changed files with 178 additions and 153 deletions

View File

@ -20,7 +20,7 @@ module.exports = Self => {
async syncUsers() { async syncUsers() {
let instance = await Self.getInstance(); let instance = await Self.getInstance();
let usersToSync = instance.getUsers(); let usersToSync = await instance.synchronizerGetUsers();
usersToSync = Array.from(usersToSync.values()) usersToSync = Array.from(usersToSync.values())
.sort((a, b) => a.localeCompare(b)); .sort((a, b) => a.localeCompare(b));
@ -28,9 +28,9 @@ module.exports = Self => {
try { try {
console.log(`Synchronizing user '${userName}'`); console.log(`Synchronizing user '${userName}'`);
await instance.synchronizerSyncUser(userName); await instance.synchronizerSyncUser(userName);
console.log(` -> '${userName}' sinchronized`); console.log(` -> User '${userName}' sinchronized`);
} catch (err) { } catch (err) {
console.error(` -> '${userName}' synchronization error:`, err.message); console.error(` -> User '${userName}' synchronization error:`, err.message);
} }
} }
@ -171,7 +171,7 @@ module.exports = Self => {
}, },
async syncUser(userName, info, password) { async syncUser(userName, info, password) {
if (info.user) if (info.user && password)
await app.models.user.setPassword(info.user.id, password); await app.models.user.setPassword(info.user.id, password);
}, },

View File

@ -35,109 +35,104 @@ module.exports = Self => {
accountConfig accountConfig
} = this; } = this;
let {user} = info; let newEntry;
let res = await client.search(this.userDn, { if (info.hasAccount) {
scope: 'sub', let {user} = info;
attributes: ['userPassword', 'sambaNTPassword'],
filter: `&(uid=${userName})`
});
let oldUser; let oldUser = await client.searchOne(this.userDn, {
await new Promise((resolve, reject) => { scope: 'sub',
res.on('error', reject); attributes: ['userPassword', 'sambaNTPassword'],
res.on('searchEntry', e => oldUser = e.object); filter: `&(uid=${userName})`
res.on('end', resolve); });
});
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 { try {
let dn = `uid=${userName},${this.userDn}`;
await client.del(dn); await client.del(dn);
operation = 'delete';
} catch (e) { } catch (e) {
if (e.name !== 'NoSuchObjectError') throw e; if (e.name !== 'NoSuchObjectError') throw e;
} }
if (!info.hasAccount) { if (info.hasAccount) {
if (oldUser) await client.add(dn, newEntry);
console.log(` -> '${userName}' removed from LDAP`); operation = 'add';
return;
} }
let nickname = user.nickname || userName; if (operation === 'delete')
let nameArgs = nickname.trim().split(' '); console.log(` -> User '${userName}' removed from LDAP`);
let sn = nameArgs.length > 1
? nameArgs.splice(1).join(' ')
: '-';
let dn = `uid=${userName},${this.userDn}`;
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];
}
await client.add(dn, newEntry);
}, },
async syncUserGroups(userName, info) { async syncUserGroups(userName, info) {
let {client} = this; let {client} = this;
let res = await client.search(this.groupDn, { let opts = {
scope: 'sub', scope: 'sub',
attributes: ['dn'], attributes: ['dn'],
filter: `&(memberUid=${userName})(objectClass=posixGroup)` filter: `&(memberUid=${userName})(objectClass=posixGroup)`
}); };
let oldGroups = await client.searchAll(this.groupDn, opts);
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 = []; let reqs = [];
for (let oldGroup of oldGroups) { for (let oldGroup of oldGroups) {
@ -167,17 +162,13 @@ module.exports = Self => {
async getUsers(usersToSync) { async getUsers(usersToSync) {
let {client} = this; let {client} = this;
let res = await client.search(this.userDn, { let opts = {
scope: 'sub', scope: 'sub',
attributes: ['uid'], attributes: ['uid'],
filter: `uid=*` filter: `uid=*`
}); };
await client.searchForeach(this.userDn, opts,
await new Promise((resolve, reject) => { o => usersToSync.add(o.uid));
res.on('error', reject);
res.on('searchEntry', e => usersToSync.add(e.object.uid));
res.on('end', resolve);
});
}, },
async syncRoles() { async syncRoles() {
@ -187,30 +178,7 @@ module.exports = Self => {
accountConfig accountConfig
} = this; } = this;
// Delete roles // Prepare data
let opts = {
scope: 'sub',
attributes: ['dn'],
filter: 'objectClass=posixGroup'
};
let res = await client.search(this.groupDn, opts);
let reqs = [];
await new Promise((resolve, reject) => {
res.on('error', err => {
if (err.name === 'NoSuchObjectError')
err = new Error(`Object '${this.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({ let roles = await $.Role.find({
fields: ['id', 'name', 'description'] fields: ['id', 'name', 'description']
@ -238,6 +206,20 @@ module.exports = Self => {
return {key: user.roleFk, val: user.name}; 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 = []; reqs = [];
for (let role of roles) { for (let role of roles) {
let newEntry = { let newEntry = {
@ -263,3 +245,15 @@ module.exports = Self => {
} }
}); });
}; };
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;
}

View File

@ -2,6 +2,14 @@
const ldap = require('../util/ldapjs-extra'); const ldap = require('../util/ldapjs-extra');
const ssh = require('node-ssh'); const ssh = require('node-ssh');
/**
* 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 => { module.exports = Self => {
Self.getSynchronizer = async function() { Self.getSynchronizer = async function() {
return await Self.findOne({ return await Self.findOne({
@ -55,8 +63,16 @@ module.exports = Self => {
async syncUser(userName, info, password) { async syncUser(userName, info, password) {
let {sshClient} = this; let {sshClient} = this;
let sambaUser = await this.adClient.searchOne(this.usersDn(), {
scope: 'sub',
attributes: ['userAccountControl'],
filter: `(&(objectClass=user)(sAMAccountName=${userName}))`
});
let isEnabled = sambaUser
&& !(sambaUser.userAccountControl & UserAccountControlFlags.ACCOUNTDISABLE);
if (info.hasAccount) { if (info.hasAccount) {
try { if (!sambaUser) {
await sshClient.exec('samba-tool user create', [ await sshClient.exec('samba-tool user create', [
userName, userName,
'--uid-number', `${info.uidNumber}`, '--uid-number', `${info.uidNumber}`,
@ -71,58 +87,42 @@ module.exports = Self => {
userName, userName,
'0027' '0027'
]); ]);
} catch (e) {} }
if (!isEnabled) {
await sshClient.exec('samba-tool user enable', [ await sshClient.exec('samba-tool user enable', [
userName userName
]); ]);
}
if (password) { if (password) {
await sshClient.exec('samba-tool user setpassword', [ await sshClient.exec('samba-tool user setpassword', [
userName, userName,
'--newpassword', password '--newpassword', password
]); ]);
} }
} else { } else if (isEnabled) {
try { await sshClient.exec('samba-tool user disable', [
await sshClient.exec('samba-tool user disable', [ userName
userName ]);
]); console.log(` -> User '${userName}' disabled on Samba`);
console.log(` -> '${userName}' disabled on Samba`);
} catch (e) {}
} }
}, },
/** /**
* Gets Samba enabled users. * Gets Samba enabled users.
* *
* Summary of userAccountControl flags:
* https://docs.microsoft.com/en-us/troubleshoot/windows-server/identity/useraccountcontrol-manipulate-account-properties
*
* @param {Set} usersToSync * @param {Set} usersToSync
*/ */
async getUsers(usersToSync) { async getUsers(usersToSync) {
let {adClient} = this; const LDAP_MATCHING_RULE_BIT_AND = '1.2.840.113556.1.4.803';
let usersDn = this.usersDn(); let filter = `!(userAccountControl:${LDAP_MATCHING_RULE_BIT_AND}:=${UserAccountControlFlags.ACCOUNTDISABLE})`;
let opts = { let opts = {
scope: 'sub', scope: 'sub',
attributes: ['sAMAccountName'], attributes: ['sAMAccountName'],
filter: '(&(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))' filter: `(&(objectClass=user)(${filter}))`
}; };
let res = await adClient.search(usersDn, opts); await this.adClient.searchForeach(this.usersDn(), opts,
o => usersToSync.add(o.sAMAccountName));
await new Promise((resolve, reject) => {
res.on('error', err => {
if (err.name === 'NoSuchObjectError')
err = new Error(`Object '${usersDn}' does not exist`);
reject(err);
});
res.on('searchEntry', e => {
usersToSync.add(e.object.sAMAccountName);
});
res.on('end', resolve);
});
} }
}); });
}; };

View File

@ -26,5 +26,36 @@ function createClient(opts) {
'starttls', 'starttls',
'unbind' 'unbind'
]); ]);
Object.assign(client, {
async searchForeach(base, options, eachFn, controls) {
let res = await this.search(base, options);
await new Promise((resolve, reject) => {
res.on('error', err => {
if (err.name === 'NoSuchObjectError')
err = new Error(`Object '${base}' does not exist`);
reject(err);
});
res.on('searchEntry', e => eachFn(e.object));
res.on('end', resolve);
});
},
async searchAll(base, options, controls) {
let elements = [];
await this.searchForeach(base, options,
o => elements.push(o), controls);
return elements;
},
async searchOne(base, options, controls) {
let object;
await this.searchForeach(base, options,
o => object = o, controls);
return object;
}
});
return client; return client;
} }