322 lines
10 KiB
JavaScript
322 lines
10 KiB
JavaScript
|
|
const app = require('vn-loopback/server/server');
|
|
const ldap = require('../util/ldapjs-extra');
|
|
const crypto = require('crypto');
|
|
const nthash = require('smbhash').nthash;
|
|
|
|
module.exports = Self => {
|
|
const shouldSync = process.env.NODE_ENV !== 'test';
|
|
|
|
Self.getLinker = async function() {
|
|
return await Self.findOne({
|
|
fields: [
|
|
'server',
|
|
'rdn',
|
|
'password',
|
|
'userDn',
|
|
'groupDn'
|
|
]
|
|
});
|
|
};
|
|
|
|
Object.assign(Self.prototype, {
|
|
async init() {
|
|
this.client = ldap.createClient({
|
|
url: this.server
|
|
});
|
|
this.client.on('error', () => {});
|
|
await this.client.bind(this.rdn, this.password);
|
|
},
|
|
|
|
async deinit() {
|
|
await this.client.unbind();
|
|
},
|
|
|
|
async syncUser(userName, info, password) {
|
|
let {
|
|
client,
|
|
accountConfig
|
|
} = this;
|
|
|
|
let dn = `uid=${userName},${this.userDn}`;
|
|
|
|
if (info.hasAccount) {
|
|
let {user} = info;
|
|
|
|
let oldUser = await client.searchOne(this.userDn, {
|
|
scope: 'sub',
|
|
filter: `&(uid=${userName})`
|
|
});
|
|
|
|
let nickname = user.nickname || userName;
|
|
let nameArgs = nickname.trim().split(' ');
|
|
let sn = nameArgs.length > 1
|
|
? nameArgs.splice(1).join(' ')
|
|
: '-';
|
|
|
|
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];
|
|
}
|
|
|
|
if (oldUser) {
|
|
let changes = [];
|
|
let skipProps = new Set([
|
|
'dn',
|
|
'controls'
|
|
]);
|
|
|
|
for (let prop in oldUser) {
|
|
let deleteProp = !skipProps.has(prop)
|
|
&& !newEntry.hasOwnProperty(prop);
|
|
if (!deleteProp) continue;
|
|
changes.push(new ldap.Change({
|
|
operation: 'delete',
|
|
modification: {
|
|
[prop]: oldUser[prop]
|
|
}
|
|
}));
|
|
}
|
|
for (let prop in newEntry) {
|
|
if (this.isEqual(oldUser[prop], newEntry[prop]))
|
|
continue;
|
|
changes.push(new ldap.Change({
|
|
operation: 'replace',
|
|
modification: {
|
|
[prop]: newEntry[prop]
|
|
}
|
|
}));
|
|
}
|
|
|
|
if (shouldSync && changes.length)
|
|
await client.modify(dn, changes);
|
|
} else if (shouldSync)
|
|
await client.add(dn, newEntry);
|
|
} else {
|
|
try {
|
|
if (shouldSync)
|
|
await client.del(dn);
|
|
// eslint-disable-next-line no-console
|
|
console.log(` -> User '${userName}' removed from LDAP`);
|
|
} catch (e) {
|
|
if (e.name !== 'NoSuchObjectError') throw e;
|
|
}
|
|
}
|
|
},
|
|
|
|
isEqual(a, b) {
|
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
if (a.length !== b.length)
|
|
return false;
|
|
for (let element of a) {
|
|
if (b.indexOf(element) === -1)
|
|
return false;
|
|
}
|
|
return true;
|
|
} else
|
|
return a == b;
|
|
},
|
|
|
|
async syncUserGroups(userName, info) {
|
|
let {client} = this;
|
|
let {user} = info;
|
|
let groupDn = this.groupDn;
|
|
|
|
let opts = {
|
|
scope: 'sub',
|
|
attributes: ['dn', 'cn'],
|
|
filter: `&(memberUid=${userName})(objectClass=posixGroup)`
|
|
};
|
|
let oldGroups = await client.searchAll(groupDn, opts);
|
|
|
|
let deleteGroups = [];
|
|
let addGroups = [];
|
|
|
|
if (info.hasAccount) {
|
|
let oldSet = new Set();
|
|
oldGroups.forEach(e => oldSet.add(e.cn));
|
|
|
|
let newSet = new Set();
|
|
user.roles().forEach(e => newSet.add(e.inherits().name));
|
|
|
|
for (let group of oldGroups) {
|
|
if (!newSet.has(group.cn))
|
|
deleteGroups.push(group.cn);
|
|
}
|
|
for (let role of user.roles()) {
|
|
if (!oldSet.has(role.inherits().name))
|
|
addGroups.push(role.inherits().name);
|
|
}
|
|
} else {
|
|
for (let group of oldGroups)
|
|
deleteGroups.push(group.cn);
|
|
}
|
|
|
|
async function applyOperations(groups, operation) {
|
|
for (let group of groups) {
|
|
try {
|
|
let dn = `cn=${group},${groupDn}`;
|
|
if (shouldSync) {
|
|
await client.modify(dn, new ldap.Change({
|
|
operation,
|
|
modification: {memberUid: userName}
|
|
}));
|
|
}
|
|
} catch (err) {
|
|
if (err.name !== 'NoSuchObjectError')
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
|
|
await applyOperations(deleteGroups, 'delete');
|
|
await applyOperations(addGroups, 'add');
|
|
},
|
|
|
|
async getUsers(usersToSync) {
|
|
let {client} = this;
|
|
|
|
let opts = {
|
|
scope: 'sub',
|
|
attributes: ['uid'],
|
|
filter: `uid=*`
|
|
};
|
|
await client.searchForeach(this.userDn, opts,
|
|
o => usersToSync.add(o.uid));
|
|
},
|
|
|
|
async syncRoles() {
|
|
let $ = app.models;
|
|
let {
|
|
client,
|
|
accountConfig
|
|
} = this;
|
|
|
|
// Prepare data
|
|
|
|
let roles = await $.VnRole.find({
|
|
fields: ['id', 'name', 'description']
|
|
});
|
|
let roleRoles = await $.RoleRole.find({
|
|
fields: ['role', 'inheritsFrom']
|
|
});
|
|
let roleMap = toMap(roleRoles, e => {
|
|
return {key: e.inheritsFrom, val: e.role};
|
|
});
|
|
|
|
let accounts = await $.Account.find({
|
|
fields: ['id'],
|
|
include: {
|
|
relation: 'user',
|
|
scope: {
|
|
fields: ['name', 'roleFk'],
|
|
where: {active: true}
|
|
}
|
|
}
|
|
});
|
|
let accountMap = toMap(accounts, e => {
|
|
let user = e.user();
|
|
if (!user) return;
|
|
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, object => {
|
|
if (shouldSync)
|
|
reqs.push(client.del(object.dn));
|
|
});
|
|
await Promise.all(reqs);
|
|
|
|
// Recreate roles
|
|
|
|
reqs = [];
|
|
for (let role of roles) {
|
|
let newEntry = {
|
|
objectClass: ['top', 'posixGroup'],
|
|
cn: role.name,
|
|
description: role.description,
|
|
gidNumber: accountConfig.idBase + role.id
|
|
};
|
|
|
|
let memberUid = [];
|
|
for (let 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},${this.groupDn}`;
|
|
if (shouldSync)
|
|
reqs.push(client.add(dn, newEntry));
|
|
}
|
|
await Promise.all(reqs);
|
|
}
|
|
});
|
|
};
|
|
|
|
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;
|
|
}
|