diff --git a/db/dump/fixtures.before.sql b/db/dump/fixtures.before.sql index 30f1ceb5e..ef8bbccc4 100644 --- a/db/dump/fixtures.before.sql +++ b/db/dump/fixtures.before.sql @@ -79,8 +79,8 @@ INSERT INTO `account`.`roleConfig`(`id`, `mysqlPassword`, `rolePrefix`, `userPre CALL `account`.`role_sync`; -INSERT INTO `account`.`user`(`id`,`name`, `nickname`, `role`,`active`,`email`, `lang`, `image`, `password`) - SELECT id, LOWER(name), CONCAT(name, 'Nick'), id, 1, CONCAT(name, '@mydomain.com'), 'en', '4fa3ada0-3ac4-11eb-9ab8-27f6fc3b85fd', '$2b$10$UzQHth.9UUQ1T5aiQJ21lOU0oVlbxoqH4PFM9V8T90KNSAcg0eEL2' +INSERT INTO `account`.`user`(`id`,`name`, `nickname`, `role`,`active`,`email`, `lang`, `image`, `password`, `sync`) + SELECT id, LOWER(name), CONCAT(name, 'Nick'), id, 1, CONCAT(name, '@mydomain.com'), 'en', '4fa3ada0-3ac4-11eb-9ab8-27f6fc3b85fd', '$2b$10$UzQHth.9UUQ1T5aiQJ21lOU0oVlbxoqH4PFM9V8T90KNSAcg0eEL2', 1 FROM `account`.`role` ORDER BY id; @@ -767,7 +767,7 @@ INSERT INTO `vn`.`ticket`(`id`, `priority`, `agencyModeFk`,`warehouseFk`,`routeF (35, 1, 1, 1, 3, util.VN_CURDATE(), util.VN_CURDATE(), 1102, 'Somewhere in Philippines', 123, NULL, 0, 1, 16, 0, util.VN_CURDATE(), NULL, NULL), (36, 1, 1, 1, 3, util.VN_CURDATE(), util.VN_CURDATE(), 1102, 'Ant-Man Adventure', 123, NULL, 0, 1, 16, 0, util.VN_CURDATE(), NULL, NULL), (37, 1, 1, 1, 3, util.VN_CURDATE(), util.VN_CURDATE(), 1110, 'Deadpool swords', 123, NULL, 0, 1, 16, 0, util.VN_CURDATE(), NULL, NULL); - + INSERT INTO `vn`.`ticketObservation`(`id`, `ticketFk`, `observationTypeFk`, `description`) VALUES (1, 11, 1, 'ready'), @@ -3836,7 +3836,7 @@ INSERT INTO `vn`.`ledgerConfig` SET INSERT INTO vn.sectorCollection SET id = 2, userFk = 18, - sectorFk = 1; + sectorFk = 1; INSERT INTO vn.sectorCollectionSaleGroup SET id = 8, @@ -3850,9 +3850,13 @@ INSERT INTO vn.saleGroup (userFk, parkingFk, sectorFk, ticketFk) INSERT INTO vn.sectorCollection SET id = 3, userFk = 18, - sectorFk = 1; + sectorFk = 1; INSERT INTO vn.sectorCollectionSaleGroup SET id = 9, sectorCollectionFk = 3, - saleGroupFk = 6; \ No newline at end of file + saleGroupFk = 6; + + +INSERT INTO account.sambaConfig (id,adDomain,adController,adUser,adPassword,verifyCert,userDn,groupDn) VALUES + (1,'verdnatura.es','192.168.56.101','Administrator','V3rdn4tur4$',0,'ou=VnUsers','ou=VnGroups'); diff --git a/db/versions/11035-blueGalax/00-firstScript.sql b/db/versions/11035-blueGalax/00-firstScript.sql new file mode 100644 index 000000000..4ba206d81 --- /dev/null +++ b/db/versions/11035-blueGalax/00-firstScript.sql @@ -0,0 +1,2 @@ +-- Place your SQL code here +ALTER TABLE account.sambaConfig ADD groupDn varchar(255) NULL COMMENT 'The base DN for groups'; diff --git a/modules/account/back/methods/account-linker/syncRoles.js b/modules/account/back/methods/account-linker/syncRoles.js new file mode 100644 index 000000000..682664bcf --- /dev/null +++ b/modules/account/back/methods/account-linker/syncRoles.js @@ -0,0 +1,17 @@ +const NotFoundError = require('vn-loopback/util/not-found-error'); + +module.exports = Self => { + Self.remoteMethod('syncRoles', { + description: 'syncRoles', + http: { + path: `/syncRoles`, + verb: 'GET' + } + }); + + Self.syncRoles = async function() { + const connector = await Self.getLinker(); + if (!connector) throw new NotFoundError('Linker not configured'); + await connector.syncRoles(); + }; +}; diff --git a/modules/account/back/mixins/account-linker.js b/modules/account/back/mixins/account-linker.js index c882d0893..d9871ddeb 100644 --- a/modules/account/back/mixins/account-linker.js +++ b/modules/account/back/mixins/account-linker.js @@ -4,6 +4,7 @@ const UserError = require('vn-loopback/util/user-error'); module.exports = function(Self, options) { require('../methods/account-linker/test')(Self); + require('../methods/account-linker/syncRoles')(Self); Self.once('attached', function() { app.models.AccountConfig.addLinker(Self); @@ -54,7 +55,15 @@ module.exports = function(Self, options) { /** * Synchronizes roles. */ - async syncRoles() {}, + async syncRoles() { + try { + await this.syncRoles(); + } catch (e) { + let err = new UserError(e.message); + err.name = e.name; + throw err; + } + }, /** * Tests linker configuration. diff --git a/modules/account/back/models/samba-config.js b/modules/account/back/models/samba-config.js index 359b4b187..db5f37f87 100644 --- a/modules/account/back/models/samba-config.js +++ b/modules/account/back/models/samba-config.js @@ -1,6 +1,6 @@ const ldap = require('../util/ldapjs-extra'); -const execFile = require('child_process').execFile; +const SambaHelper = require('./samba-helper'); const isProduction = require('vn-loopback/server/boot/isProduction'); /** @@ -24,6 +24,7 @@ module.exports = Self => { 'adUser', 'adPassword', 'userDn', + 'groupDn', 'verifyCert' ] }); @@ -46,38 +47,16 @@ module.exports = Self => { Object.assign(this, { adClient, fullUsersDn: `${this.userDn},${baseDn}`, + fullGroupsDn: `${this.groupDn},${baseDn}`, bindDn }); + this.sambaHandler = new SambaHelper(this); }, async deinit() { await this.adClient.unbind(); }, - async sambaTool(command, args = []) { - let authArgs = [ - '--URL', `ldaps://${this.adController}`, - '--simple-bind-dn', this.bindDn, - '--password', this.adPassword - ]; - if (!this.verifyCert) - authArgs.push('--option', 'tls verify peer = no_check'); - - const allArgs = [command].concat( - args, authArgs - ); - - if (!shouldSync) return; - return await new Promise((resolve, reject) => { - execFile('samba-tool', allArgs, (err, stdout, stderr) => { - if (err) - reject(err); - else - resolve({stdout, stderr}); - }); - }); - }, - async getAdUser(userName) { const sambaUser = await this.adClient.searchOne(this.fullUsersDn, { scope: 'sub', @@ -105,7 +84,7 @@ module.exports = Self => { if (info.hasAccount) { if (!sambaUser) { - await this.sambaTool('user', [ + await this.sambaHandler.sambaTool('user', [ 'create', userName, '--userou', this.userDn, '--random-password' @@ -113,7 +92,7 @@ module.exports = Self => { sambaUser = await this.getAdUser(userName); } if (password) { - await this.sambaTool('user', [ + await this.sambaHandler.sambaTool('user', [ 'setpassword', userName, '--newpassword', password ]); @@ -169,6 +148,17 @@ module.exports = Self => { }; await this.adClient.searchForeach(this.fullUsersDn, opts, o => usersToSync.add(o.sAMAccountName)); - } + }, + async syncRoles() { + await this.init(); + // Prepare data + try { + await this.sambaHandler.syncFromDB(); + await this.sambaHandler.syncMembers(); + } catch (error) { + console.error(error); + } + }, }); }; + diff --git a/modules/account/back/models/samba-config.json b/modules/account/back/models/samba-config.json index 4c9e0a794..6fde4f97a 100644 --- a/modules/account/back/models/samba-config.json +++ b/modules/account/back/models/samba-config.json @@ -32,6 +32,10 @@ "type": "string", "required": true }, + "groupDn": { + "type": "string", + "required": true + }, "verifyCert": { "type": "boolean" } diff --git a/modules/account/back/models/samba-helper.js b/modules/account/back/models/samba-helper.js new file mode 100644 index 000000000..713ed71c7 --- /dev/null +++ b/modules/account/back/models/samba-helper.js @@ -0,0 +1,257 @@ +const {differences, printResults} = require('../util/helpers'); +const ldap = require('../util/ldapjs-extra'); + +const ROLE_PREFIX = '$'; +const app = require('vn-loopback/server/server'); + +module.exports = class SambaHelper { + constructor(ctx) { + const {verifyCert, adPassword, adController, groupDn, userDn} = ctx; + Object.assign(this, {...ctx, verifyCert, adPassword, adController, groupDn, userDn}); + } + + async sambaTool(command, args = []) { + let authArgs = [ + '--URL', `ldaps://${this.adController}`, + '--simple-bind-dn', this.bindDn, + '--password', this.adPassword + ]; + if (!this.verifyCert) + authArgs.push('--option', 'tls verify peer = no_check'); + + const allArgs = [command].concat( + args, authArgs + ); + + if (!shouldSync) return; + return await new Promise((resolve, reject) => { + execFile('samba-tool', allArgs, (err, stdout, stderr) => { + if (err) + reject(err); + else + resolve({stdout, stderr}); + }); + }); + } + + 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]); + } + + 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() { + // await this.adClient.del( + // `cn=developer,${this.fullGroupsDn}`, + + // ); + // 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); + } + + if (this.rolesToUpdate.length > 0) { + for await (const role of this.rolesToUpdate) { + if (this.roles.get(role).$description != sambaCurrentGroups.get(role).description) { + const groupDescriptionAttribute = { + description: this.roles.get(role).$description, + }; + const changed = await this.adClient.modify( + `cn=${role},${this.fullGroupsDn}`, + new ldap.Change({ + operation: 'replace', + modification: groupDescriptionAttribute, + }) + ); + console.log(changed); + // 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); + } + } + } +}; diff --git a/modules/account/back/models/specs/samba-config.spec.js b/modules/account/back/models/specs/samba-config.spec.js new file mode 100644 index 000000000..a1d780a6c --- /dev/null +++ b/modules/account/back/models/specs/samba-config.spec.js @@ -0,0 +1,6 @@ + +xdescribe('Samba config', () => { + it('SyncRoles', async() => { + + }); +}); diff --git a/modules/account/back/util/helpers.js b/modules/account/back/util/helpers.js new file mode 100644 index 000000000..0e5ad9b9c --- /dev/null +++ b/modules/account/back/util/helpers.js @@ -0,0 +1,70 @@ +module.exports = { + toMap, + binarySearch, + differences, + printResults, + handleExecResponse +}; + +function handleExecResponse({stdin, stdout}) { + return stdout.split('\n'); +} +function printResults(results) { + // eslint-disable-next-line no-console + results.forEach(({stdout}) => console.log(stdout)); +} + +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; +} +function binarySearch(array, value) { + let first = 0; + + let last = array.length - 1; + + while (first <= last) { + const index = Math.floor((first + last) / 2); + + const middle = array[index]; + + if (middle === value) + return index; // Elemento encontrado, devuelve la posición. + + if (middle < value) + first = index + 1; + else + last = index - 1; + } + + return -1; // Elemento no encontrado. +} + +function differences(array1, array2) { + const differences = []; + + // Ordena ambos arrays + + const sortedArray1 = array1.slice().sort(); + const sortedArray2 = array2.slice().sort(); + + for (const value of sortedArray1) { + // Busca el elemento en el array ordenado utilizando búsqueda binaria + + const result = binarySearch(sortedArray2, value); + + // Si el elemento no se encuentra, agrégalo a la lista de diferencias + + if (result === -1) + differences.push(value); + } + + return differences; +} diff --git a/modules/account/front/samba/index.html b/modules/account/front/samba/index.html index 0186cac7c..37501592e 100644 --- a/modules/account/front/samba/index.html +++ b/modules/account/front/samba/index.html @@ -45,6 +45,11 @@ ng-model="$ctrl.config.userDn" rule="SambaConfig"> + +