WIP: #5770 - Sync Groups Samba #1946

Draft
jsegarra wants to merge 38 commits from 5770_sambaGroups into dev
2 changed files with 224 additions and 209 deletions
Showing only changes of commit a2e4a65c3a - Show all commits

View File

@ -1,8 +1,7 @@
const app = require('vn-loopback/server/server');
const ldap = require('../util/ldapjs-extra'); const ldap = require('../util/ldapjs-extra');
const {differences, toMap, printResults} = require('../util/helpers'); const SambaHelper = require('./samba-helper');
const execFile = require('child_process').execFile; const execFile = require('child_process').execFile;
const ROLE_PREFIX = '$';
/** /**
* Summary of userAccountControl flags: * Summary of userAccountControl flags:
* https://docs.microsoft.com/en-us/troubleshoot/windows-server/identity/useraccountcontrol-manipulate-account-properties * https://docs.microsoft.com/en-us/troubleshoot/windows-server/identity/useraccountcontrol-manipulate-account-properties
@ -14,6 +13,7 @@ const UserAccountControlFlags = {
module.exports = Self => { module.exports = Self => {
const shouldSync = process.env.NODE_ENV !== 'test'; const shouldSync = process.env.NODE_ENV !== 'test';
let sambaHandler = null;
Self.getLinker = async function() { Self.getLinker = async function() {
return await Self.findOne({ return await Self.findOne({
@ -50,6 +50,7 @@ module.exports = Self => {
fullGroupsDn: `${this.groupDn},${baseDn}`, fullGroupsDn: `${this.groupDn},${baseDn}`,
bindDn bindDn
}); });
this.sambaHandler = new SambaHelper(this);
}, },
async deinit() { async deinit() {
@ -175,10 +176,9 @@ module.exports = Self => {
async syncRoles() { async syncRoles() {
await this.init(); await this.init();
// Prepare data // Prepare data
const sambaHandler = new SambaHelper(this);
try { try {
await sambaHandler.syncFromDB(); await this.sambaHandler.syncFromDB();
await sambaHandler.syncMembers(); await this.sambaHandler.syncMembers();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
@ -186,206 +186,3 @@ module.exports = Self => {
}); });
}; };
class SambaHelper {
constructor(ctx) {
const {sambaTool, verifyCert, adPassword, adController} = ctx;
Object.assign(this, {...ctx, verifyCert, adPassword, adController, sambaTool});
}
async getRoles() {
this.roles = (await app.models.VnRole.find({
fields: ['id', 'name', 'description'],
order: 'modified DESC',
limit: 2
})).reduce((map, role) => {
map.set(`${ROLE_PREFIX}${role.name}`, role);
return map;
}, new Map());
}
async getUsers() {
this.users = await app.models.VnUser.find({
include: {
relation: 'role',
scope: {fields: ['name'],
where: {'name': {nin: this.rolesToDelete}}
}
},
fields: ['name', 'roleFk'],
// where: {'active': true}
}).reduce((map, user) => {
const role = user.role();
map.set(`${ROLE_PREFIX}${role.name}`, user.name);
return map;
}, new Map());
// this.usersMap = toMap(this.users, user => {
// let role = user.role();
// if (!role) {
// console.info(`User ${user.name} has not valid role`);
// return;
// }
// return {key: `${ROLE_PREFIX}${role.name}`, val: user.name};
// });
}
deleteRole(role) {
return this.sambaTool('group', ['delete', role]);
}
addRole({description, name}) {
return this.sambaTool('group',
['add', `${ROLE_PREFIX}${name}`, `--groupou=${this.groupDn}`, `--description=${description}`]);
}
getRoleMembers(role) {
return this.getMembers(`(cn=${role})`, this.fullGroupsDn);
}
getMembers(filter = '', type = this.fullUsersDn) {
const options = {
scope: 'sub',
attributes: ['cn', 'member']
};
if (filter !== '')
Object.assign(options, {filter});
return this.adClient.searchAll(type, options);
}
removeMembers(role, user) {
return this.sambaTool('group', ['removemembers', role, user]);
}
addMembers(role, user) {
return this.sambaTool('group', ['addmembers', role, user]);
}
editGroup(role, descriptrion = '') {
return this.sambaTool('group', ['edit', `${role}`, '--editor=\'vi\'']);
}
handleRoleMembers(users) {
if (users.length === 0) return [];
let members = users[0]?.member;
if (!members) return [];
if (!Array.isArray(members))members = [members];
return members.map((member => member.match(/CN=(.*?),(.*)/)[1]));
}
async handleUsersRole(role, currentUsers, users) {
const forbiddenUsers = ['guest'];
users = users.filter(u => !u.includes(forbiddenUsers));
const usersToDelete = differences(currentUsers, users);
if (usersToDelete.length > 0) {
const results = await Promise.all(usersToDelete.map(user =>
this.removemembers(role, user)));
printResults(results);
}
const usersToInsert = differences(users, currentUsers);
if (usersToInsert.length > 0) {
const results = await Promise.all(usersToInsert.map(user =>
this.addmembers(role, user)));
printResults(results);
}
}
async syncFromDB() {
// OBTENER ROLES
await this.getRoles();
const rolesKeys = Array.from(this.roles.keys()).sort();
// OBTENER LDAPSJS ROLES
const ldapGroups = await this.adClient.searchAll(this.fullGroupsDn, {
scope: 'sub',
attributes: ['cn', 'description'],
});
// OBTENER SAMBA ROLES
let sambaCurrentGroups = ldapGroups
.filter(group => Object.prototype.hasOwnProperty.call(group, 'cn'))
.reduce((map, group) => {
map.set(`${group.cn}`, group);
return map;
}, new Map());
const sambaRolesKeys = Array.from(sambaCurrentGroups.keys()).sort();// .map(({cn}) => cn);
// handleExecResponse(await this.sambaTool('group', ['list']))
// .filter(group => group.startsWith(ROLE_PREFIX));
// Encontrar elementos a insertar
this.rolesToInsert = [];
// Encontrar elementos a actualizar
this.rolesToUpdate = [];
for (const role of rolesKeys) {
const exists = sambaCurrentGroups.get(role);
if (!exists)
this.rolesToInsert.push(role);
if (exists)
this.rolesToUpdate.push(role);
}
// Encontrar elementos a eliminar
this.rolesToDelete = differences(sambaRolesKeys, rolesKeys);
if (this.rolesToDelete.length > 0) {
// PROCEDIMIENTO PARA ELIMINAR ROLES
const resultsRoleDelete = await Promise.all(
this.rolesToDelete.map(this.deleteRole)
);
printResults(resultsRoleDelete);
}
if (this.rolesToInsert.length > 0) {
// PROCEDIMIENTO PARA INSERTAR ROLES
const resultsRoleInsert = await Promise.all(
this.rolesToInsert.map(role => this.addRole(this.roles.get(role))));
printResults(resultsRoleInsert);
}
// const edit = await this.editGroup('$developer');
if (this.rolesToUpdate.length > 0) {
for await (const role of this.rolesToUpdate) {
await this.deleteRole(role);
await this.addRole(this.roles.get(role));
}
}
}
async syncMembers() {
// const baseDN = 'cn=Users,dc=verdnatura,dc=es';
const ldapMembersGroups = await this.getMembers();
// OBTENER USUARIOS Y SUS ROLES
if (
this.rolesToInsert.length > 0 ||
this.rolesToUpdate.length > 0)
await this.getUsers();
// PROCEDIMIENTO PARA ELIMINAR USUARIOS ASOCIADOS AL ROL
// if (this.rolesToDelete.length > 0) {
// let usersToUngroup = this.rolesToDelete.flatMap(role => {
// const exist = this.users.get(role);
// if (exist) {
// return this.users.get(role)?.map(
// user => this.removeMembers(role, user)
// );
// } else return [];
// }
// );
// const resultsUsersUngroup = await Promise.all(usersToUngroup);
// printResults(resultsUsersUngroup);
// }
if (this.rolesToInsert.length > 0) {
// PROCEDIMIENTO PARA INSERTAR USUARIOS ASOCIADOS AL ROL
let usersToGroup = this.rolesToInsert.flatMap(role => this.users.get(role).map(
user => this.addMembers(role, user)
)
);
const resultsUserGroup = await Promise.all(usersToGroup);
printResults(resultsUserGroup);
}
if (this.rolesToUpdate.length > 0) {
// OBTENER LDAPSJS MIEMBROS ROLES
for await (const role of this.rolesToUpdate) {
const users = users.get(role);
const currentUsers = this.handleRoleMembers(await this.getRoleMembers(role));
if (currentUsers.length === 0 && users.length === 0) continue;
await this.handleUsersRole(role, currentUsers, users);
}
}
}
}

View File

@ -0,0 +1,218 @@
const {differences, printResults} = require('../util/helpers');
const ROLE_PREFIX = '$';
const app = require('vn-loopback/server/server');
module.exports = class SambaHelper {
constructor(ctx) {
const {sambaTool, verifyCert, adPassword, adController, groupDn, userDn} = ctx;
Object.assign(this, {...ctx, verifyCert, adPassword, adController, groupDn, userDn, sambaTool});
}
async getRoles() {
this.roles = (await app.models.VnRole.find({
fields: ['id', 'name', 'description'],
order: 'modified DESC',
limit: 2
})).reduce((map, role) => {
map.set(`${ROLE_PREFIX}${role.name}`, role);
return map;
}, new Map());
}
async getUsers() {
this.users = await app.models.VnUser.find({
include: {
relation: 'role',
scope: {fields: ['name'],
where: {'name': {nin: this.rolesToDelete}}
}
},
fields: ['name', 'roleFk'],
// where: {'active': true}
}).reduce((map, user) => {
const role = user.role();
const roleName = `${ROLE_PREFIX}${role.name}`;
if (map.has(roleName))
map.get(roleName).push(user.name);
else {
// Si no existe, creamos una nueva lista con el nombre del usuario y la asociamos a la clave
map.set(roleName, [user.name]);
}
return map;
}, new Map());
// this.usersMap = toMap(this.users, user => {
// let role = user.role();
// if (!role) {
// console.info(`User ${user.name} has not valid role`);
// return;
// }
// return {key: `${ROLE_PREFIX}${role.name}`, val: user.name};
// });
}
deleteRole(role) {
return this.sambaTool('group', ['delete', role]);
}
addRole({description, name}) {
return this.sambaTool('group',
['add', `${ROLE_PREFIX}${name}`, `--groupou=${this.groupDn}`, `--description=${description}`]);
}
getRoleMembers(role) {
return this.getMembers(`(cn=${role})`, this.fullGroupsDn);
}
getMembers(filter = '', type = this.fullUsersDn) {
const options = {
scope: 'sub',
attributes: ['cn', 'member', 'member.cn']
};
if (filter !== '')
Object.assign(options, {filter});
return this.adClient.searchAll(type, options);
}
removeMembers(role, user) {
return this.sambaTool('group', ['removemembers', role, user]);
}
addMembers(role, user) {
return this.sambaTool('group', ['addmembers', role, user]);
}
editGroup(role, descriptrion = '') {
return this.sambaTool('group', ['edit', `${role}`, '--editor=\'vi\'']);
}
handleRoleMembers(users) {
if (users.length === 0) return [];
let members = users[0]?.member;
if (!members) return [];
if (!Array.isArray(members))members = [members];
return members.map((member => member.match(/CN=(.*?),(.*)/)[1]));
}
async handleUsersRole(role, currentUsers, users) {
const usersToDelete = differences(currentUsers, users);
if (usersToDelete.length > 0) {
const results = await Promise.all(usersToDelete.map(user =>
this.removeMembers(role, user)));
printResults(results);
}
const usersToInsert = differences(users, currentUsers);
if (usersToInsert.length > 0) {
const results = await Promise.all(usersToInsert.map(user =>
this.addMembers(role, user)));
printResults(results);
}
}
notFilterMap(map) {
return ([clave, valor]) => !map.has(clave);
}
filterMap(map) {
return ([clave, valor]) => map.has(clave);
}
getKeysMap(map) {
return Array.from(map.keys());
}
async syncFromDB() {
// OBTENER ROLES
await this.getRoles();
// const rolesKeys = Array.from(this.roles.keys()).sort();
// OBTENER LDAPSJS ROLES
const ldapGroups = await this.adClient.searchAll(this.fullGroupsDn, {
scope: 'sub',
attributes: ['cn', 'description'],
});
// OBTENER SAMBA ROLES
let sambaCurrentGroups = ldapGroups
.filter(group => Object.prototype.hasOwnProperty.call(group, 'cn'))
.reduce((map, group) => {
map.set(`${group.cn}`, group);
return map;
}, new Map());
// const sambaRolesKeys = Array.from(sambaCurrentGroups.keys()).sort();// .map(({cn}) => cn);
// handleExecResponse(await this.sambaTool('group', ['list']))
// .filter(group => group.startsWith(ROLE_PREFIX));
// Encontrar elementos a insertar
this.rolesToInsert = this.getKeysMap(new Map([...this.roles].filter(this.notFilterMap(sambaCurrentGroups))));
// Encontrar elementos a actualizar
this.rolesToUpdate = this.getKeysMap(new Map([...this.roles].filter(this.filterMap(sambaCurrentGroups))));
// Encontrar elementos a eliminar
this.rolesToDelete = this.getKeysMap(new Map([...sambaCurrentGroups].filter(this.notFilterMap(this.roles))));
if (this.rolesToDelete.length > 0) {
// PROCEDIMIENTO PARA ELIMINAR ROLES
const resultsRoleDelete = await Promise.all(
this.rolesToDelete.map(this.deleteRole)
);
printResults(resultsRoleDelete);
}
if (this.rolesToInsert.length > 0) {
// PROCEDIMIENTO PARA INSERTAR ROLES
const resultsRoleInsert = await Promise.all(
this.rolesToInsert.map(role => this.addRole(this.roles.get(role))));
printResults(resultsRoleInsert);
}
// const edit = await this.editGroup('$developer');
if (this.rolesToUpdate.length > 0) {
for await (const role of this.rolesToUpdate) {
await this.deleteRole(role);
await this.addRole(this.roles.get(role));
}
}
}
async syncMembers() {
// const baseDN = 'cn=Users,dc=verdnatura,dc=es';
const ldapMembersGroups = await this.getMembers();
// OBTENER USUARIOS Y SUS ROLES
if (
this.rolesToInsert.length > 0 ||
this.rolesToUpdate.length > 0)
await this.getUsers();
// PROCEDIMIENTO PARA ELIMINAR USUARIOS ASOCIADOS AL ROL
// if (this.rolesToDelete.length > 0) {
// let usersToUngroup = this.rolesToDelete.flatMap(role => {
// const exist = this.users.get(role);
// if (exist) {
// return this.users.get(role)?.map(
// user => this.removeMembers(role, user)
// );
// } else return [];
// }
// );
// const resultsUsersUngroup = await Promise.all(usersToUngroup);
// printResults(resultsUsersUngroup);
// }
if (this.rolesToInsert.length > 0) {
// PROCEDIMIENTO PARA INSERTAR USUARIOS ASOCIADOS AL ROL
let usersToGroup = this.rolesToInsert.flatMap(role => this.users.get(role).map(
user => this.addMembers(role, user)
)
);
const resultsUserGroup = await Promise.all(usersToGroup);
printResults(resultsUserGroup);
}
if (this.rolesToUpdate.length > 0) {
// OBTENER LDAPSJS MIEMBROS ROLES
const forbiddenUsers = ['$guest'];
this.rolesToUpdate = this.rolesToUpdate.filter(role => !forbiddenUsers.includes(role));
for await (const role of this.rolesToUpdate) {
const users = this.users.get(role);
const currentUsers = this.handleRoleMembers(await this.getRoleMembers(role));
if (currentUsers.length === 0 && users.length === 0) continue;
await this.handleUsersRole(role, currentUsers, users);
}
}
}
};