diff --git a/back/methods/account/privileges.js b/back/methods/account/privileges.js new file mode 100644 index 000000000..df421125e --- /dev/null +++ b/back/methods/account/privileges.js @@ -0,0 +1,58 @@ +const UserError = require('vn-loopback/util/user-error'); + +module.exports = Self => { + Self.remoteMethodCtx('privileges', { + description: 'Change role and hasGrant if user has privileges', + accepts: [ + { + arg: 'id', + type: 'number', + required: true, + description: 'The user id', + http: {source: 'path'} + }, + { + arg: 'roleFk', + type: 'number', + description: 'The new role for user', + }, + { + arg: 'hasGrant', + type: 'boolean', + description: 'Whether to has grant' + } + ], + http: { + path: `/:id/privileges`, + verb: 'POST' + } + }); + + Self.privileges = async function(ctx, id, roleFk, hasGrant, options) { + const models = Self.app.models; + const userId = ctx.req.accessToken.userId; + + const myOptions = {}; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + const user = await models.Account.findById(userId, null, myOptions); + + if (!user.hasGrant) + throw new UserError(`You don't have enough privileges`); + + const userToUpdate = await models.Account.findById(id); + if (hasGrant != null) + return await userToUpdate.updateAttribute('hasGrant', hasGrant, myOptions); + if (!roleFk) return; + + const role = await models.Role.findById(roleFk, null, myOptions); + const hasRole = await models.Account.hasRole(userId, role.name, myOptions); + + if (!hasRole) + throw new UserError(`You don't have enough privileges`); + + await userToUpdate.updateAttribute('roleFk', roleFk, myOptions); + }; +}; diff --git a/back/methods/account/specs/privileges.spec.js b/back/methods/account/specs/privileges.spec.js new file mode 100644 index 000000000..137c08671 --- /dev/null +++ b/back/methods/account/specs/privileges.spec.js @@ -0,0 +1,99 @@ +const models = require('vn-loopback/server/server').models; + +describe('account privileges()', () => { + const employeeId = 1; + const developerId = 9; + const sysadminId = 66; + const bruceWayneId = 1101; + + it('should throw an error when user not has privileges', async() => { + const ctx = {req: {accessToken: {userId: developerId}}}; + const tx = await models.Account.beginTransaction({}); + + let error; + try { + const options = {transaction: tx}; + + await models.Account.privileges(ctx, employeeId, null, true, options); + + await tx.rollback(); + } catch (e) { + error = e; + await tx.rollback(); + } + + expect(error.message).toContain(`You don't have enough privileges`); + }); + + it('should throw an error when user has privileges but not has the role', async() => { + const ctx = {req: {accessToken: {userId: sysadminId}}}; + const tx = await models.Account.beginTransaction({}); + + let error; + try { + const options = {transaction: tx}; + + const root = await models.Role.findOne({ + where: { + name: 'root' + } + }, options); + await models.Account.privileges(ctx, employeeId, root.id, null, options); + + await tx.rollback(); + } catch (e) { + error = e; + await tx.rollback(); + } + + expect(error.message).toContain(`You don't have enough privileges`); + }); + + it('should change role', async() => { + const ctx = {req: {accessToken: {userId: sysadminId}}}; + const tx = await models.Account.beginTransaction({}); + + const options = {transaction: tx}; + const agency = await models.Role.findOne({ + where: { + name: 'agency' + } + }, options); + + let error; + let result; + try { + await models.Account.privileges(ctx, bruceWayneId, agency.id, null, options); + result = await models.Account.findById(bruceWayneId, null, options); + + await tx.rollback(); + } catch (e) { + error = e; + await tx.rollback(); + } + + expect(error).not.toBeDefined(); + expect(result.roleFk).toEqual(agency.id); + }); + + it('should change hasGrant', async() => { + const ctx = {req: {accessToken: {userId: sysadminId}}}; + const tx = await models.Account.beginTransaction({}); + + let error; + let result; + try { + const options = {transaction: tx}; + await models.Account.privileges(ctx, bruceWayneId, null, true, options); + result = await models.Account.findById(bruceWayneId, null, options); + + await tx.rollback(); + } catch (e) { + error = e; + await tx.rollback(); + } + + expect(error).not.toBeDefined(); + expect(result.hasGrant).toBeTruthy(); + }); +}); diff --git a/back/models/account.js b/back/models/account.js index ba703c68d..f74052b5c 100644 --- a/back/models/account.js +++ b/back/models/account.js @@ -7,6 +7,7 @@ module.exports = Self => { require('../methods/account/change-password')(Self); require('../methods/account/set-password')(Self); require('../methods/account/validate-token')(Self); + require('../methods/account/privileges')(Self); // Validations @@ -77,7 +78,7 @@ module.exports = Self => { `SELECT r.name FROM account.user u JOIN account.roleRole rr ON rr.role = u.role - JOIN account.role r ON r.id = rr.inheritsFrom + JOIN account.role r ON r.id = rr.inheritsFrom WHERE u.id = ?`, [userId], options); let roles = []; diff --git a/back/models/account.json b/back/models/account.json index 5f0b05f9b..c25cd532d 100644 --- a/back/models/account.json +++ b/back/models/account.json @@ -48,6 +48,9 @@ }, "image": { "type": "string" + }, + "hasGrant": { + "type": "boolean" } }, "relations": { diff --git a/db/changes/10490-august/00-user_hasGrant.sql b/db/changes/10490-august/00-user_hasGrant.sql new file mode 100644 index 000000000..60d1273d8 --- /dev/null +++ b/db/changes/10490-august/00-user_hasGrant.sql @@ -0,0 +1 @@ +ALTER TABLE `account`.`user` ADD hasGrant TINYINT(1) NOT NULL; diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index 73a6ef687..7e59c1a54 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -2663,3 +2663,7 @@ INSERT INTO `vn`.`collection` (`id`, `created`, `workerFk`, `stateFk`, `itemPack INSERT INTO `vn`.`ticketCollection` (`ticketFk`, `collectionFk`, `created`, `level`, `wagon`, `smartTagFk`, `usedShelves`, `itemCount`, `liters`) VALUES (9, 3, util.VN_NOW(), NULL, 0, NULL, NULL, NULL, NULL); + +UPDATE `account`.`user` + SET `hasGrant` = 1 + WHERE `id` = 66; diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js index df9088140..fedcaf5f9 100644 --- a/e2e/helpers/selectors.js +++ b/e2e/helpers/selectors.js @@ -51,14 +51,12 @@ export default { accountDescriptor: { menuButton: 'vn-user-descriptor vn-icon-button[icon="more_vert"]', deleteAccount: '.vn-menu [name="deleteUser"]', - changeRole: '.vn-menu [name="changeRole"]', setPassword: '.vn-menu [name="setPassword"]', activateAccount: '.vn-menu [name="enableAccount"]', activateUser: '.vn-menu [name="activateUser"]', deactivateUser: '.vn-menu [name="deactivateUser"]', newPassword: 'vn-textfield[ng-model="$ctrl.newPassword"]', repeatPassword: 'vn-textfield[ng-model="$ctrl.repeatPassword"]', - newRole: 'vn-autocomplete[ng-model="$ctrl.newRole"]', activeAccountIcon: 'vn-icon[icon="contact_mail"]', activeUserIcon: 'vn-icon[icon="icon-disabled"]', acceptButton: 'button[response="accept"]', @@ -143,6 +141,11 @@ export default { verifyCert: 'vn-account-samba vn-check[ng-model="$ctrl.config.verifyCert"]', save: 'vn-account-samba vn-submit' }, + accountPrivileges: { + checkHasGrant: 'vn-user-privileges vn-check[ng-model="$ctrl.user.hasGrant"]', + role: 'vn-user-privileges vn-autocomplete[ng-model="$ctrl.user.roleFk"]', + save: 'vn-user-privileges vn-submit' + }, clientsIndex: { createClientButton: `vn-float-button` }, diff --git a/e2e/paths/14-account/01_create_and_basic_data.spec.js b/e2e/paths/14-account/01_create_and_basic_data.spec.js index 0400fb99e..54e4d1f12 100644 --- a/e2e/paths/14-account/01_create_and_basic_data.spec.js +++ b/e2e/paths/14-account/01_create_and_basic_data.spec.js @@ -62,27 +62,6 @@ describe('Account create and basic data path', () => { }); describe('Descriptor option', () => { - describe('Edit role', () => { - it('should edit the role using the descriptor menu', async() => { - await page.waitToClick(selectors.accountDescriptor.menuButton); - await page.waitToClick(selectors.accountDescriptor.changeRole); - await page.autocompleteSearch(selectors.accountDescriptor.newRole, 'adminBoss'); - await page.waitToClick(selectors.accountDescriptor.acceptButton); - const message = await page.waitForSnackbar(); - - expect(message.text).toContain('Role changed succesfully!'); - }); - - it('should reload the roles section to see now there are more roles', async() => { - // when role updates db takes time to return changes, without this timeout the result would have been 3 - await page.waitForTimeout(1000); - await page.reloadSection('account.card.roles'); - const rolesCount = await page.countElement(selectors.accountRoles.anyResult); - - expect(rolesCount).toEqual(61); - }); - }); - describe('activate account', () => { it(`should check the active account icon isn't present in the descriptor`, async() => { await page.waitForNumberOfElements(selectors.accountDescriptor.activeAccountIcon, 0); diff --git a/e2e/paths/14-account/09_privileges.spec.js b/e2e/paths/14-account/09_privileges.spec.js new file mode 100644 index 000000000..71e9345a8 --- /dev/null +++ b/e2e/paths/14-account/09_privileges.spec.js @@ -0,0 +1,86 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Account privileges path', () => { + let browser; + let page; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('developer', 'account'); + await page.accessToSearchResult('1101'); + await page.accessToSection('account.card.privileges'); + }); + + afterAll(async() => { + await browser.close(); + }); + + describe('as developer', () => { + it('should throw error when give privileges', async() => { + await page.waitToClick(selectors.accountPrivileges.checkHasGrant); + await page.waitToClick(selectors.accountPrivileges.save); + + const message = await page.waitForSnackbar(); + + expect(message.text).toContain(`You don't have enough privileges`); + }); + + it('should throw error when change role', async() => { + await page.autocompleteSearch(selectors.accountPrivileges.role, 'employee'); + await page.waitToClick(selectors.accountPrivileges.save); + + const message = await page.waitForSnackbar(); + + expect(message.text).toContain(`You don't have enough privileges`); + }); + }); + + describe('as sysadmin', () => { + beforeAll(async() => { + await page.loginAndModule('sysadmin', 'account'); + await page.accessToSearchResult('9'); + await page.accessToSection('account.card.privileges'); + }); + + it('should give privileges', async() => { + await page.waitToClick(selectors.accountPrivileges.checkHasGrant); + await page.waitToClick(selectors.accountPrivileges.save); + const message = await page.waitForSnackbar(); + + await page.reloadSection('account.card.privileges'); + const result = await page.checkboxState(selectors.accountPrivileges.checkHasGrant); + + expect(message.text).toContain(`Data saved!`); + expect(result).toBe('checked'); + }); + + it('should change role', async() => { + await page.autocompleteSearch(selectors.accountPrivileges.role, 'employee'); + await page.waitToClick(selectors.accountPrivileges.save); + const message = await page.waitForSnackbar(); + + await page.reloadSection('account.card.privileges'); + const result = await page.waitToGetProperty(selectors.accountPrivileges.role, 'value'); + + expect(message.text).toContain(`Data saved!`); + expect(result).toContain('employee'); + }); + }); + + describe('as developer again', () => { + it('should remove privileges', async() => { + await page.accessToSearchResult('9'); + await page.accessToSection('account.card.privileges'); + + await page.waitToClick(selectors.accountPrivileges.checkHasGrant); + await page.waitToClick(selectors.accountPrivileges.save); + + await page.reloadSection('account.card.privileges'); + const result = await page.checkboxState(selectors.accountPrivileges.checkHasGrant); + + expect(result).toBe('unchecked'); + }); + }); +}); diff --git a/modules/account/front/descriptor/index.html b/modules/account/front/descriptor/index.html index c709c1ec0..7a7ba43f3 100644 --- a/modules/account/front/descriptor/index.html +++ b/modules/account/front/descriptor/index.html @@ -11,14 +11,6 @@ translate> Delete - - Change role - - @@ -128,22 +120,6 @@ question="Are you sure you want to continue?" message="User will be deactivated"> - - - - - - - - - - - \ No newline at end of file + diff --git a/modules/account/front/descriptor/index.js b/modules/account/front/descriptor/index.js index 3f27b1f76..b802b2349 100644 --- a/modules/account/front/descriptor/index.js +++ b/modules/account/front/descriptor/index.js @@ -30,20 +30,6 @@ class Controller extends Descriptor { .then(() => this.vnApp.showSuccess(this.$t('User removed'))); } - onChangeRole() { - this.newRole = this.user.role.id; - this.$.changeRole.show(); - } - - onChangeRoleAccept() { - const params = {roleFk: this.newRole}; - return this.$http.patch(`Accounts/${this.id}`, params) - .then(() => { - this.emit('change'); - this.vnApp.showSuccess(this.$t('Role changed succesfully!')); - }); - } - onChangePassClick(askOldPass) { this.$http.get('UserPasswords/findOne') .then(res => { diff --git a/modules/account/front/descriptor/index.spec.js b/modules/account/front/descriptor/index.spec.js index 8ee67a304..f5e7aa7d4 100644 --- a/modules/account/front/descriptor/index.spec.js +++ b/modules/account/front/descriptor/index.spec.js @@ -30,17 +30,6 @@ describe('component vnUserDescriptor', () => { }); }); - describe('onChangeRoleAccept()', () => { - it('should call backend method to change role', () => { - $httpBackend.expectPATCH('Accounts/1').respond(); - controller.onChangeRoleAccept(); - $httpBackend.flush(); - - expect(controller.vnApp.showSuccess).toHaveBeenCalled(); - expect(controller.emit).toHaveBeenCalledWith('change'); - }); - }); - describe('onPassChange()', () => { it('should throw an error when password is empty', () => { expect(() => { diff --git a/modules/account/front/index.js b/modules/account/front/index.js index 50e04c9fa..0cd0c4955 100644 --- a/modules/account/front/index.js +++ b/modules/account/front/index.js @@ -18,3 +18,4 @@ import './roles'; import './ldap'; import './samba'; import './accounts'; +import './privileges'; diff --git a/modules/account/front/privileges/index.html b/modules/account/front/privileges/index.html new file mode 100644 index 000000000..e3e44898a --- /dev/null +++ b/modules/account/front/privileges/index.html @@ -0,0 +1,42 @@ + + + +
+ + + + + + + + + + + + + + + + +
diff --git a/modules/account/front/privileges/index.js b/modules/account/front/privileges/index.js new file mode 100644 index 000000000..00ba772df --- /dev/null +++ b/modules/account/front/privileges/index.js @@ -0,0 +1,9 @@ +import ngModule from '../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section {} + +ngModule.component('vnUserPrivileges', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/account/front/privileges/locale/es.yml b/modules/account/front/privileges/locale/es.yml new file mode 100644 index 000000000..f7330e1be --- /dev/null +++ b/modules/account/front/privileges/locale/es.yml @@ -0,0 +1,2 @@ +Privileges: Privilegios +Has grant: Tiene privilegios diff --git a/modules/account/front/routes.json b/modules/account/front/routes.json index 66b26f427..b96c931c9 100644 --- a/modules/account/front/routes.json +++ b/modules/account/front/routes.json @@ -19,7 +19,8 @@ {"state": "account.card.basicData", "icon": "settings"}, {"state": "account.card.roles", "icon": "group"}, {"state": "account.card.mailForwarding", "icon": "forward"}, - {"state": "account.card.aliases", "icon": "email"} + {"state": "account.card.aliases", "icon": "email"}, + {"state": "account.card.privileges", "icon": "badge"} ], "role": [ {"state": "account.role.card.basicData", "icon": "settings"}, @@ -99,6 +100,13 @@ "description": "Mail aliases", "acl": ["marketing", "hr"] }, + { + "url": "/privileges", + "state": "account.card.privileges", + "component": "vn-user-privileges", + "description": "Privileges", + "acl": ["hr"] + }, { "url": "/role?q", "state": "account.role", @@ -249,4 +257,4 @@ "acl": ["developer"] } ] -} \ No newline at end of file +}