Account synchronization fixes
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Juan Ferrer 2020-11-13 13:20:37 +01:00
parent 9652711d9a
commit 6e5eac1f78
4 changed files with 178 additions and 153 deletions

View File

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

View File

@ -35,109 +35,104 @@ module.exports = Self => {
accountConfig
} = this;
let {user} = info;
let newEntry;
let res = await client.search(this.userDn, {
scope: 'sub',
attributes: ['userPassword', 'sambaNTPassword'],
filter: `&(uid=${userName})`
});
if (info.hasAccount) {
let {user} = info;
let oldUser;
await new Promise((resolve, reject) => {
res.on('error', reject);
res.on('searchEntry', e => oldUser = e.object);
res.on('end', resolve);
});
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 {
let dn = `uid=${userName},${this.userDn}`;
await client.del(dn);
operation = 'delete';
} catch (e) {
if (e.name !== 'NoSuchObjectError') throw e;
}
if (!info.hasAccount) {
if (oldUser)
console.log(` -> '${userName}' removed from LDAP`);
return;
if (info.hasAccount) {
await client.add(dn, newEntry);
operation = 'add';
}
let nickname = user.nickname || userName;
let nameArgs = nickname.trim().split(' ');
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);
if (operation === 'delete')
console.log(` -> User '${userName}' removed from LDAP`);
},
async syncUserGroups(userName, info) {
let {client} = this;
let res = await client.search(this.groupDn, {
let opts = {
scope: 'sub',
attributes: ['dn'],
filter: `&(memberUid=${userName})(objectClass=posixGroup)`
});
let oldGroups = [];
await new Promise((resolve, reject) => {
res.on('error', reject);
res.on('searchEntry', e => oldGroups.push(e.object));
res.on('end', resolve);
});
};
let oldGroups = await client.searchAll(this.groupDn, opts);
let reqs = [];
for (let oldGroup of oldGroups) {
@ -167,17 +162,13 @@ module.exports = Self => {
async getUsers(usersToSync) {
let {client} = this;
let res = await client.search(this.userDn, {
let opts = {
scope: 'sub',
attributes: ['uid'],
filter: `uid=*`
});
await new Promise((resolve, reject) => {
res.on('error', reject);
res.on('searchEntry', e => usersToSync.add(e.object.uid));
res.on('end', resolve);
});
};
await client.searchForeach(this.userDn, opts,
o => usersToSync.add(o.uid));
},
async syncRoles() {
@ -187,30 +178,7 @@ module.exports = Self => {
accountConfig
} = this;
// Delete roles
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
// Prepare data
let roles = await $.Role.find({
fields: ['id', 'name', 'description']
@ -238,6 +206,20 @@ module.exports = Self => {
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 = {
@ -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 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 => {
Self.getSynchronizer = async function() {
return await Self.findOne({
@ -55,8 +63,16 @@ module.exports = Self => {
async syncUser(userName, info, password) {
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) {
try {
if (!sambaUser) {
await sshClient.exec('samba-tool user create', [
userName,
'--uid-number', `${info.uidNumber}`,
@ -71,58 +87,42 @@ module.exports = Self => {
userName,
'0027'
]);
} catch (e) {}
await sshClient.exec('samba-tool user enable', [
userName
]);
}
if (!isEnabled) {
await sshClient.exec('samba-tool user enable', [
userName
]);
}
if (password) {
await sshClient.exec('samba-tool user setpassword', [
userName,
'--newpassword', password
]);
}
} else {
try {
await sshClient.exec('samba-tool user disable', [
userName
]);
console.log(` -> '${userName}' disabled on Samba`);
} catch (e) {}
} else if (isEnabled) {
await sshClient.exec('samba-tool user disable', [
userName
]);
console.log(` -> User '${userName}' disabled on Samba`);
}
},
/**
* 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
*/
async getUsers(usersToSync) {
let {adClient} = this;
let usersDn = this.usersDn();
const LDAP_MATCHING_RULE_BIT_AND = '1.2.840.113556.1.4.803';
let filter = `!(userAccountControl:${LDAP_MATCHING_RULE_BIT_AND}:=${UserAccountControlFlags.ACCOUNTDISABLE})`;
let opts = {
scope: 'sub',
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 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);
});
await this.adClient.searchForeach(this.usersDn(), opts,
o => usersToSync.add(o.sAMAccountName));
}
});
};

View File

@ -26,5 +26,36 @@ function createClient(opts) {
'starttls',
'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;
}