LDAP & Samba synchronization fixes + improvements
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Juan Ferrer 2020-10-31 00:02:27 +01:00
parent 5e5dcf26c6
commit 9b563cb8bf
10 changed files with 37974 additions and 3666 deletions

View File

@ -9,7 +9,7 @@
"properties": {
"id": {
"type": "number",
"required": true
"id": true
},
"name": {
"type": "string",
@ -56,7 +56,8 @@
"roles": {
"type": "hasMany",
"model": "RoleRole",
"foreignKey": "role"
"foreignKey": "role",
"primaryKey": "roleFk"
},
"emailUser": {
"type": "hasOne",

View File

@ -0,0 +1,3 @@
ALTER TABLE `account`.`roleRole`
ADD `id` INT UNSIGNED NOT NULL AUTO_INCREMENT FIRST,
ADD PRIMARY KEY (`id`);

11466
front/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -62,31 +62,27 @@ module.exports = Self => {
let roles = await $.Role.find({
fields: ['id', 'name']
});
let roleRoles = await $.RoleRole.find({
fields: ['role', 'inheritsFrom']
});
let roleMap = toMap(roleRoles, e => {
return {key: e.inheritsFrom, val: e.role};
});
let accounts = await $.UserAccount.find({
fields: ['id'],
include: {
relation: 'user',
scope: {
fields: ['name'],
include: {
relation: 'roles',
scope: {
fields: ['inheritsFrom']
}
}
fields: ['name', 'roleFk'],
where: {active: true}
}
}
});
let map = new Map();
for (let account of accounts) {
let user = account.user();
for (let inherit of user.roles()) {
let roleId = inherit.inheritsFrom;
if (!map.has(roleId)) map.set(roleId, []);
map.get(roleId).push(user.name);
}
}
let accountMap = toMap(accounts, e => {
let user = e.user();
return {key: user.roleFk, val: user.name};
});
reqs = [];
for (let role of roles) {
@ -96,8 +92,14 @@ module.exports = Self => {
gidNumber: accountConfig.idBase + role.id
};
let memberUid = map.get(role.id);
if (memberUid) newEntry.memberUid = memberUid;
let memberUid = [];
for (subrole of roleMap.get(role.id) || [])
memberUid = memberUid.concat(accountMap.get(subrole) || []);
if (memberUid.length) {
memberUid.sort((a, b) => a.localeCompare(b));
newEntry.memberUid = memberUid;
}
let dn = `cn=${role.name},${ldapConfig.groupDn}`;
reqs.push(client.add(dn, newEntry));
@ -112,3 +114,14 @@ module.exports = Self => {
if (err) throw err;
};
};
function toMap(array, fn) {
let map = new Map();
for (let item of array) {
let keyVal = fn(item);
let key = keyVal.key;
if (!map.has(key)) map.set(key, []);
map.get(key).push(keyVal.val);
}
return map;
}

View File

@ -0,0 +1,104 @@
const ldap = require('../../util/ldapjs-extra');
const ssh = require('node-ssh');
module.exports = Self => {
Self.remoteMethod('syncAll', {
description: 'Synchronizes user database with LDAP and Samba',
http: {
path: `/syncAll`,
verb: 'PATCH'
}
});
Self.syncAll = async function() {
let $ = Self.app.models;
let usersToSync = new Set();
// Database
let accounts = await $.UserAccount.find({
fields: ['id'],
include: {
relation: 'user',
scope: {
fields: ['name'],
where: {active: true}
}
}
});
for (let account of accounts)
usersToSync.add(account.user().name);
// LDAP
let ldapConfig = await $.LdapConfig.findOne({
fields: ['host', 'rdn', 'password', 'baseDn']
});
if (ldapConfig) {
let ldapClient = ldap.createClient({
url: `ldap://${ldapConfig.host}:389`
});
let ldapPassword = Buffer
.from(ldapConfig.password, 'base64')
.toString('ascii');
await ldapClient.bind(ldapConfig.rdn, ldapPassword);
res = await ldapClient.search(ldapConfig.baseDn, {
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);
});
// FIXME: Cannot disconnect, hangs on undind() call
// await ldapClient.unbind();
}
// Samba
let sambaConfig = await $.SambaConfig.findOne({
fields: ['host', 'sshUser', 'sshPass']
});
if (sambaConfig) {
let sshPassword = Buffer
.from(sambaConfig.sshPass, 'base64')
.toString('ascii');
let sshClient = new ssh.NodeSSH();
await sshClient.connect({
host: sambaConfig.host,
username: sambaConfig.sshUser,
password: sshPassword
});
let res = await sshClient.execCommand('samba-tool user list');
let users = res.stdout.split('\n');
for (let user of users) usersToSync.add(user.trim());
await sshClient.dispose();
}
// Syncing
$.RoleInherit.sync();
let sync = await Self.syncInit();
for (let user of usersToSync.values()) {
try {
await Self.doSync(sync, user);
} catch (err) {
console.error(err);
}
}
await Self.syncDeinit(sync);
};
};

View File

@ -19,7 +19,7 @@ module.exports = Self => {
}
],
http: {
path: `/sync`,
path: `/:userName/sync`,
verb: 'PATCH'
}
});
@ -35,45 +35,121 @@ module.exports = Self => {
if (user && isSync) return;
let accountConfig;
let mailConfig;
let sync = await Self.syncInit();
try {
await Self.doSync(sync, userName, password);
} finally {
await Self.syncDeinit(sync);
}
};
Self.syncInit = async function() {
let $ = Self.app.models;
let accountConfig = await $.AccountConfig.findOne({
fields: ['homedir', 'shell', 'idBase']
});
let mailConfig = await $.MailConfig.findOne({
fields: ['domain']
});
// LDAP
let ldapClient;
let ldapConfig = await $.LdapConfig.findOne({
fields: ['host', 'rdn', 'password', 'baseDn', 'groupDn']
});
if (ldapConfig) {
ldapClient = ldap.createClient({
url: `ldap://${ldapConfig.host}:389`
});
let ldapPassword = Buffer
.from(ldapConfig.password, 'base64')
.toString('ascii');
await ldapClient.bind(ldapConfig.rdn, ldapPassword);
}
// Samba
let sambaClient;
let sambaConfig = await $.SambaConfig.findOne({
fields: ['host', 'sshUser', 'sshPass']
});
if (sambaConfig) {
let sshPassword = Buffer
.from(sambaConfig.sshPass, 'base64')
.toString('ascii');
sambaClient = new ssh.NodeSSH();
await sambaClient.connect({
host: sambaConfig.host,
username: sambaConfig.sshUser,
password: sshPassword
});
}
return {
accountConfig,
mailConfig,
ldapConfig,
ldapClient,
sambaClient
};
};
Self.syncDeinit = async function(sync) {
// FIXME: Cannot disconnect, hangs on undind() call
// if (sync.ldapClient)
// await sync.ldapClient.unbind();
if (sync.sambaClient)
await sync.sambaClient.dispose();
};
Self.doSync = async function(sync, userName, password) {
let $ = Self.app.models;
let {
accountConfig,
mailConfig,
ldapConfig,
ldapClient,
sambaClient
} = sync;
let user = await $.Account.findOne({
where: {name: userName},
fields: [
'id',
'nickname',
'email',
'lang',
'roleFk',
'sync',
'active',
'created',
'bcryptPassword',
'updated'
],
include: {
relation: 'roles',
scope: {
include: {
relation: 'inherits',
scope: {
fields: ['name']
}
}
}
}
});
let extraParams;
let hasAccount = false;
if (user) {
accountConfig = await $.AccountConfig.findOne({
fields: ['homedir', 'shell', 'idBase']
});
mailConfig = await $.MailConfig.findOne({
fields: ['domain']
});
user = await $.Account.findById(user.id, {
fields: [
'id',
'nickname',
'email',
'lang',
'roleFk',
'sync',
'active',
'created',
'updated'
],
where: {name: userName},
include: {
relation: 'roles',
scope: {
include: {
relation: 'inherits',
scope: {
fields: ['name']
}
}
}
}
});
extraParams = {
corporateMail: `${userName}@${mailConfig.domain}`,
uidNumber: accountConfig.idBase + user.id
@ -83,8 +159,12 @@ module.exports = Self => {
&& await $.UserAccount.exists(user.id);
}
// Database
if (user) {
let bcryptPassword = $.User.hashPassword(password);
let bcryptPassword = password
? $.User.hashPassword(password)
: user.bcryptPassword;
await $.Account.upsertWithWhere({id: user.id},
{bcryptPassword}
@ -101,7 +181,7 @@ module.exports = Self => {
// SIP
if (hasAccount) {
if (hasAccount && password) {
await Self.rawSql('CALL pbx.sip_setPassword(?, ?)',
[user.id, password]
);
@ -109,155 +189,169 @@ module.exports = Self => {
// LDAP
let ldapConfig = await $.LdapConfig.findOne({
fields: ['host', 'rdn', 'password', 'baseDn', 'groupDn']
});
if (ldapClient) {
// Deletes user
if (ldapConfig) {
let ldapClient = ldap.createClient({
url: `ldap://${ldapConfig.host}:389`
res = await ldapClient.search(ldapConfig.baseDn, {
scope: 'sub',
attributes: ['userPassword', 'sambaNTPassword'],
filter: `&(uid=${userName})(objectClass=inetOrgPerson)`
});
let ldapPassword = Buffer
.from(ldapConfig.password, 'base64')
.toString('ascii');
await ldapClient.bind(ldapConfig.rdn, ldapPassword);
let oldUser;
await new Promise((resolve, reject) => {
res.on('error', reject);
res.on('searchEntry', e => oldUser = e.object);
res.on('end', resolve);
});
let err;
try {
// Deletes user
try {
let dn = `uid=${userName},${ldapConfig.baseDn}`;
await ldapClient.del(dn);
} catch (e) {
if (e.name !== 'NoSuchObjectError') throw e;
}
// Removes user from groups
let opts = {
scope: 'sub',
attributes: ['dn'],
filter: `&(memberUid=${userName})(objectClass=posixGroup)`
};
res = await ldapClient.search(ldapConfig.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 = [];
for (oldGroup of oldGroups) {
let change = new ldap.Change({
operation: 'delete',
modification: {memberUid: userName}
});
reqs.push(ldapClient.modify(oldGroup.dn, change));
}
await Promise.all(reqs);
if (hasAccount) {
// Recreates user
let nameArgs = user.nickname.split(' ');
let sshaPassword = crypto
.createHash('sha1')
.update(password)
.digest('base64');
let dn = `uid=${userName},${ldapConfig.baseDn}`;
let newEntry = {
uid: userName,
objectClass: [
'inetOrgPerson',
'posixAccount',
'sambaSamAccount'
],
cn: user.nickname || userName,
displayName: user.nickname,
givenName: nameArgs[0],
sn: nameArgs[1] || 'Empty',
mail: extraParams.corporateMail,
userPassword: `{SSHA}${sshaPassword}`,
preferredLanguage: user.lang,
homeDirectory: `${accountConfig.homedir}/${userName}`,
loginShell: accountConfig.shell,
uidNumber: extraParams.uidNumber,
gidNumber: accountConfig.idBase + user.roleFk,
sambaSID: '-',
sambaNTPassword: nthash(password)
};
await ldapClient.add(dn, newEntry);
// Adds user to groups
let reqs = [];
for (let role of user.roles()) {
let change = new ldap.Change({
operation: 'add',
modification: {memberUid: userName}
});
let roleName = role.inherits().name;
let dn = `cn=${roleName},${ldapConfig.groupDn}`;
reqs.push(ldapClient.modify(dn, change));
}
await Promise.all(reqs);
}
let dn = `uid=${userName},${ldapConfig.baseDn}`;
await ldapClient.del(dn);
} catch (e) {
err = e;
if (e.name !== 'NoSuchObjectError') throw e;
}
// FIXME: Cannot disconnect, hangs on undind() call
// await ldapClient.unbind();
if (err) throw err;
// Removes user from groups
res = await ldapClient.search(ldapConfig.groupDn, {
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 reqs = [];
for (oldGroup of oldGroups) {
let change = new ldap.Change({
operation: 'delete',
modification: {memberUid: userName}
});
reqs.push(ldapClient.modify(oldGroup.dn, change));
}
await Promise.all(reqs);
if (hasAccount) {
// Recreates user
let nameArgs = user.nickname.split(' ');
let sn = nameArgs.length > 1
? nameArgs.splice(1).join(' ')
: '-';
let dn = `uid=${userName},${ldapConfig.baseDn}`;
let newEntry = {
uid: userName,
objectClass: [
'inetOrgPerson',
'posixAccount',
'sambaSamAccount'
],
cn: user.nickname || userName,
displayName: user.nickname,
givenName: nameArgs[0],
sn,
mail: extraParams.corporateMail,
preferredLanguage: user.lang,
homeDirectory: `${accountConfig.homedir}/${userName}`,
loginShell: accountConfig.shell,
uidNumber: extraParams.uidNumber,
gidNumber: accountConfig.idBase + user.roleFk,
sambaSID: '-'
};
let passwords;
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');
passwords = {
userPassword: `{SSHA}${ssha}`,
sambaNTPassword: nthash(password)
};
} else if (oldUser) {
passwords = {
userPassword: oldUser.userPassword,
sambaNTPassword: oldUser.sambaNTPassword
};
}
if (passwords)
Object.assign(newEntry, passwords);
await ldapClient.add(dn, newEntry);
// Adds user to groups
let reqs = [];
for (let role of user.roles()) {
let change = new ldap.Change({
operation: 'add',
modification: {memberUid: userName}
});
let roleName = role.inherits().name;
let dn = `cn=${roleName},${ldapConfig.groupDn}`;
reqs.push(ldapClient.modify(dn, change));
}
await Promise.all(reqs);
}
}
// Samba
let sambaConfig = await $.SambaConfig.findOne({
fields: ['host', 'sshUser', 'sshPass']
});
if (sambaConfig) {
let sshPassword = Buffer
.from(sambaConfig.sshPass, 'base64')
.toString('ascii');
let sshClient = new ssh.NodeSSH();
await sshClient.connect({
host: sambaConfig.host,
username: sambaConfig.sshUser,
password: sshPassword
});
let commands;
if (sambaClient) {
if (hasAccount) {
commands = [
`samba-tool user create "${userName}" `
+ `--uid-number=${extraParams.uidNumber} `
+ `--mail-address="${extraParams.corporateMail}" `
+ `--random-password`,
`samba-tool user setexpiry "${userName}" `
+ `--noexpiry`,
`samba-tool user setpassword "${userName}" `
+ `--newpassword="${password}"`,
`mkhomedir_helper "${userName}" 0027`
];
try {
await sambaClient.exec('samba-tool user create', [
userName,
'--uid-number', `${extraParams.uidNumber}`,
'--mail-address', extraParams.corporateMail,
'--random-password'
]);
} catch (e) {}
await sambaClient.exec('samba-tool user setexpiry', [
userName,
'--noexpiry'
]);
if (password) {
await sambaClient.exec('samba-tool user setpassword', [
userName,
'--newpassword', password
]);
await sambaClient.exec('samba-tool user enable', [
userName
]);
}
await sambaClient.exec('mkhomedir_helper', [
userName,
'0027'
]);
} else {
commands = [
`samba-tool user delete "${userName}"`
];
try {
await sambaClient.exec('samba-tool user disable', [
userName
]);
} catch (e) {}
}
for (let command of commands)
await sshClient.execCommand(command);
await sshClient.dispose();
}
// Mark as synchronized

View File

@ -7,7 +7,7 @@
}
},
"properties": {
"role": {
"id": {
"id": true
}
},

View File

@ -2,4 +2,5 @@
module.exports = Self => {
require('../methods/user-account/sync')(Self);
require('../methods/user-account/sync-by-id')(Self);
require('../methods/user-account/sync-all')(Self);
};

27827
package-lock.json generated

File diff suppressed because it is too large Load Diff

1743
print/package-lock.json generated

File diff suppressed because it is too large Load Diff