Account synchronization fixes
gitea/salix/pipeline/head This commit looks good
Details
gitea/salix/pipeline/head This commit looks good
Details
This commit is contained in:
parent
9652711d9a
commit
6e5eac1f78
|
@ -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);
|
||||
},
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue