From 0e7e804475de24d22033c44211576858473d3fcc Mon Sep 17 00:00:00 2001 From: jorgep Date: Fri, 3 May 2024 13:18:46 +0200 Subject: [PATCH 1/6] feat: refs #6598 create useAcl composable --- src/composables/useAcl.js | 35 +++++++++++++++++++++++++++++++++++ src/composables/useSession.js | 2 ++ src/composables/useState.js | 11 +++++++++++ src/router/index.js | 2 ++ 4 files changed, 50 insertions(+) create mode 100644 src/composables/useAcl.js diff --git a/src/composables/useAcl.js b/src/composables/useAcl.js new file mode 100644 index 000000000..3121646f1 --- /dev/null +++ b/src/composables/useAcl.js @@ -0,0 +1,35 @@ +import { useState } from './useState'; +import axios from 'axios'; + +export function useAcl() { + const state = useState(); + + async function fetch() { + const { data } = await axios.get('VnUsers/acls'); + const acls = {}; + data.forEach((acl) => { + acls[acl.model] = acls[acl.model] || {}; + acls[acl.model][acl.property] = acls[acl.model][acl.property] || {}; + acls[acl.model][acl.property][acl.accessType] = true; + }); + + state.setAcls(acls); + } + + function hasAny(model, property, accessType) { + const acls = acls[model]; + if (acls) { + for (const prop of ['*', property]) { + const acl = acls[prop]; + if (acl && (acl['*'] || acl[accessType])) return true; + } + } + return false; + } + + return { + fetch, + hasAny, + state, + }; +} diff --git a/src/composables/useSession.js b/src/composables/useSession.js index 56bce0279..ca2abef00 100644 --- a/src/composables/useSession.js +++ b/src/composables/useSession.js @@ -1,5 +1,6 @@ import { useState } from './useState'; import { useRole } from './useRole'; +import { useAcl } from './useAcl'; import { useUserConfig } from './useUserConfig'; import axios from 'axios'; import useNotify from './useNotify'; @@ -88,6 +89,7 @@ export function useSession() { setSession(data); await useRole().fetch(); + await useAcl().fetch(); await useUserConfig().fetch(); await useTokenConfig().fetch(); diff --git a/src/composables/useState.js b/src/composables/useState.js index e671d41bd..95b9fdfd2 100644 --- a/src/composables/useState.js +++ b/src/composables/useState.js @@ -13,6 +13,7 @@ const user = ref({ }); const roles = ref([]); +const acls = ref([]); const tokenConfig = ref({}); const drawer = ref(true); const headerMounted = ref(false); @@ -53,6 +54,14 @@ export function useState() { function setRoles(data) { roles.value = data; } + + function getAcls() { + return computed(() => acls.value); + } + + function setAcls(data) { + acls.value = data; + } function getTokenConfig() { return computed(() => { return tokenConfig.value; @@ -80,6 +89,8 @@ export function useState() { setUser, getRoles, setRoles, + getAcls, + setAcls, getTokenConfig, setTokenConfig, set, diff --git a/src/router/index.js b/src/router/index.js index 7a0aedcae..41ff4c1da 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -13,6 +13,7 @@ import { useRole } from 'src/composables/useRole'; import { useUserConfig } from 'src/composables/useUserConfig'; import { toLowerCamel } from 'src/filters'; import { useTokenConfig } from 'src/composables/useTokenConfig'; +import { useAcl } from 'src/composables/useAcl'; const state = useState(); const session = useSession(); @@ -55,6 +56,7 @@ export default route(function (/* { store, ssrContext } */) { const stateRoles = state.getRoles().value; if (stateRoles.length === 0) { await useRole().fetch(); + await useAcl().fetch(); await useUserConfig().fetch(); await useTokenConfig().fetch(); } From 12ff55a010487a4f6717b191b400471a2f3bbb74 Mon Sep 17 00:00:00 2001 From: jorgep Date: Wed, 15 May 2024 17:24:01 +0200 Subject: [PATCH 2/6] fix: refs #6598 get acls --- src/composables/useAcl.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/composables/useAcl.js b/src/composables/useAcl.js index 3121646f1..362a7c551 100644 --- a/src/composables/useAcl.js +++ b/src/composables/useAcl.js @@ -1,5 +1,5 @@ -import { useState } from './useState'; import axios from 'axios'; +import { useState } from './useState'; export function useAcl() { const state = useState(); @@ -17,14 +17,13 @@ export function useAcl() { } function hasAny(model, property, accessType) { - const acls = acls[model]; - if (acls) { - for (const prop of ['*', property]) { - const acl = acls[prop]; - if (acl && (acl['*'] || acl[accessType])) return true; - } - } - return false; + const modelAcls = state.getAcls().value[model]; + if (!modelAcls) return false; + + return ['*', property].some(prop => { + const acl = modelAcls[prop]; + return acl && (acl['*'] || acl[accessType]); + }); } return { From 74e330e523e0aa9a82c79f2e817b613e8e44aeff Mon Sep 17 00:00:00 2001 From: jorgep Date: Thu, 16 May 2024 14:05:39 +0200 Subject: [PATCH 3/6] fix: refs #6598 fix unit tests --- .../__tests__/composables/useSession.spec.js | 17 ++++++++--------- test/vitest/__tests__/pages/Login/Login.spec.js | 5 +++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/test/vitest/__tests__/composables/useSession.spec.js b/test/vitest/__tests__/composables/useSession.spec.js index 2292859a9..6e08807cf 100644 --- a/test/vitest/__tests__/composables/useSession.spec.js +++ b/test/vitest/__tests__/composables/useSession.spec.js @@ -1,5 +1,5 @@ import { vi, describe, expect, it, beforeAll, beforeEach } from 'vitest'; -import { axios, flushPromises } from 'app/test/vitest/helper'; +import { axios } from 'app/test/vitest/helper'; import { useSession } from 'composables/useSession'; import { useState } from 'composables/useState'; @@ -7,6 +7,7 @@ const session = useSession(); const state = useState(); describe('session', () => { + describe('getToken / setToken', () => { it('should return an empty string if no token is found in local or session storage', async () => { const expectedToken = ''; @@ -87,13 +88,15 @@ describe('session', () => { }, }, ]; + beforeEach(() => { + vi.spyOn(axios, 'get').mockImplementation((url) => { + if(url === 'VnUsers/acls') return Promise.resolve({data: []}); + return Promise.resolve({data: { roles: rolesData, user: expectedUser }}); + }); + }) it('should fetch the user roles and then set token in the sessionStorage', async () => { const expectedRoles = ['salesPerson', 'admin']; - vi.spyOn(axios, 'get').mockResolvedValue({ - data: { roles: rolesData, user: expectedUser }, - }); - const expectedToken = 'mySessionToken'; const expectedTokenMultimedia = 'mySessionTokenMultimedia'; const keepLogin = false; @@ -117,10 +120,6 @@ describe('session', () => { it('should fetch the user roles and then set token in the localStorage', async () => { const expectedRoles = ['salesPerson', 'admin']; - vi.spyOn(axios, 'get').mockResolvedValue({ - data: { roles: rolesData, user: expectedUser }, - }); - const expectedToken = 'myLocalToken'; const expectedTokenMultimedia = 'myLocalTokenMultimedia'; const keepLogin = true; diff --git a/test/vitest/__tests__/pages/Login/Login.spec.js b/test/vitest/__tests__/pages/Login/Login.spec.js index 6e2de9870..9b9968736 100644 --- a/test/vitest/__tests__/pages/Login/Login.spec.js +++ b/test/vitest/__tests__/pages/Login/Login.spec.js @@ -23,8 +23,9 @@ describe('Login', () => { }, }; vi.spyOn(axios, 'post').mockResolvedValueOnce({ data: { token: 'token' } }); - vi.spyOn(axios, 'get').mockResolvedValue({ - data: { roles: [], user: expectedUser , multimediaToken: {id:'multimediaToken' }}, + vi.spyOn(axios, 'get').mockImplementation((url) => { + if (url === 'VnUsers/acls') return Promise.resolve({ data: [] }); + return Promise.resolve({data: { roles: [], user: expectedUser , multimediaToken: {id:'multimediaToken' }}}); }); vi.spyOn(vm.quasar, 'notify'); From 1d67233e691c0c7cdd03b273a4c1039c880d1af7 Mon Sep 17 00:00:00 2001 From: jorgep Date: Thu, 16 May 2024 16:29:42 +0200 Subject: [PATCH 4/6] feat: refs #6598 unit tests --- src/composables/useAcl.js | 15 +++-- .../__tests__/composables/useAcl.spec.js | 57 +++++++++++++++++++ .../__tests__/composables/useSession.spec.js | 9 +-- 3 files changed, 69 insertions(+), 12 deletions(-) create mode 100644 test/vitest/__tests__/composables/useAcl.spec.js diff --git a/src/composables/useAcl.js b/src/composables/useAcl.js index 362a7c551..46aaa3c25 100644 --- a/src/composables/useAcl.js +++ b/src/composables/useAcl.js @@ -16,14 +16,13 @@ export function useAcl() { state.setAcls(acls); } - function hasAny(model, property, accessType) { - const modelAcls = state.getAcls().value[model]; - if (!modelAcls) return false; - - return ['*', property].some(prop => { - const acl = modelAcls[prop]; - return acl && (acl['*'] || acl[accessType]); - }); + function hasAny(model, prop, accessType) { + const acls = state.getAcls().value[model]; + if (acls) + return ['*', prop].some((key) => { + const acl = acls[key]; + return acl && (acl['*'] || acl[accessType]); + }); } return { diff --git a/test/vitest/__tests__/composables/useAcl.spec.js b/test/vitest/__tests__/composables/useAcl.spec.js new file mode 100644 index 000000000..d468720e5 --- /dev/null +++ b/test/vitest/__tests__/composables/useAcl.spec.js @@ -0,0 +1,57 @@ +import { vi, describe, expect, it, beforeAll, afterEach, beforeEach } from 'vitest'; +import { axios, flushPromises } from 'app/test/vitest/helper'; +import { useAcl } from 'src/composables/useAcl'; + +describe('useAcl', () => { + const acl = useAcl(); + const mockAcls = [ + { + model: 'Address', + property: '*', + accessType: '*', + permission: 'ALLOW', + principalType: 'ROLE', + principalId: 'employee', + id: 3, + }, + { + model: 'Worker', + property: 'holidays', + accessType: 'READ', + permission: 'ALLOW', + principalType: 'ROLE', + principalId: 'employee', + id: 13, + }, + ]; + const expectedAcls = { + Address: { '*': { '*': true } }, + Worker: { holidays: { READ: true } }, + }; + beforeAll(() => { + vi.spyOn(axios, 'get').mockResolvedValue({ data: mockAcls }); + vi.spyOn(acl.state, 'setAcls'); + }); + + beforeEach(async () => await acl.fetch()); + + afterEach(async () => { + await flushPromises(); + acl.state.setAcls([]); + }); + + describe('fetch', () => { + it('should call setUser and setRoles of the state with the expected data', async () => { + expect(acl.state.setAcls).toHaveBeenCalledWith(expectedAcls); + }); + }); + + describe('hasAny', () => { + it('should return true if a role matched', async () => + expect(acl.hasAny('Address', '*', 'WRITE')).toBeTruthy()); + + it('should return false if no roles matched', async () => { + expect(acl.hasAny('Worker', '*', 'WRITE')).toBeFalsy(); + }); + }); +}); diff --git a/test/vitest/__tests__/composables/useSession.spec.js b/test/vitest/__tests__/composables/useSession.spec.js index 6e08807cf..831acbf18 100644 --- a/test/vitest/__tests__/composables/useSession.spec.js +++ b/test/vitest/__tests__/composables/useSession.spec.js @@ -7,7 +7,6 @@ const session = useSession(); const state = useState(); describe('session', () => { - describe('getToken / setToken', () => { it('should return an empty string if no token is found in local or session storage', async () => { const expectedToken = ''; @@ -90,10 +89,12 @@ describe('session', () => { ]; beforeEach(() => { vi.spyOn(axios, 'get').mockImplementation((url) => { - if(url === 'VnUsers/acls') return Promise.resolve({data: []}); - return Promise.resolve({data: { roles: rolesData, user: expectedUser }}); + if (url === 'VnUsers/acls') return Promise.resolve({ data: [] }); + return Promise.resolve({ + data: { roles: rolesData, user: expectedUser }, + }); }); - }) + }); it('should fetch the user roles and then set token in the sessionStorage', async () => { const expectedRoles = ['salesPerson', 'admin']; From 12811692eb5c799f915863dfedd9177dbdd6fd56 Mon Sep 17 00:00:00 2001 From: jorgep Date: Fri, 17 May 2024 12:09:42 +0200 Subject: [PATCH 5/6] feat: refs #6598 add more tests --- .../__tests__/composables/useAcl.spec.js | 79 +++++++++++++------ 1 file changed, 55 insertions(+), 24 deletions(-) diff --git a/test/vitest/__tests__/composables/useAcl.spec.js b/test/vitest/__tests__/composables/useAcl.spec.js index d468720e5..a2b44b5e7 100644 --- a/test/vitest/__tests__/composables/useAcl.spec.js +++ b/test/vitest/__tests__/composables/useAcl.spec.js @@ -1,4 +1,4 @@ -import { vi, describe, expect, it, beforeAll, afterEach, beforeEach } from 'vitest'; +import { vi, describe, expect, it, beforeAll, afterAll } from 'vitest'; import { axios, flushPromises } from 'app/test/vitest/helper'; import { useAcl } from 'src/composables/useAcl'; @@ -12,7 +12,6 @@ describe('useAcl', () => { permission: 'ALLOW', principalType: 'ROLE', principalId: 'employee', - id: 3, }, { model: 'Worker', @@ -21,37 +20,69 @@ describe('useAcl', () => { permission: 'ALLOW', principalType: 'ROLE', principalId: 'employee', - id: 13, + }, + { + model: 'Url', + property: 'getByUser', + accessType: 'READ', + permission: 'ALLOW', + principalType: 'ROLE', + principalId: '$everyone', + }, + { + model: 'TpvTransaction', + property: 'start', + accessType: 'WRITE', + permission: 'ALLOW', + principalType: 'ROLE', + principalId: '$authenticated', }, ]; - const expectedAcls = { - Address: { '*': { '*': true } }, - Worker: { holidays: { READ: true } }, - }; - beforeAll(() => { + + beforeAll(async () => { vi.spyOn(axios, 'get').mockResolvedValue({ data: mockAcls }); - vi.spyOn(acl.state, 'setAcls'); + await acl.fetch(); }); - beforeEach(async () => await acl.fetch()); - - afterEach(async () => { - await flushPromises(); - acl.state.setAcls([]); - }); - - describe('fetch', () => { - it('should call setUser and setRoles of the state with the expected data', async () => { - expect(acl.state.setAcls).toHaveBeenCalledWith(expectedAcls); - }); - }); + afterAll(async () => await flushPromises()); describe('hasAny', () => { - it('should return true if a role matched', async () => - expect(acl.hasAny('Address', '*', 'WRITE')).toBeTruthy()); + it('should return false if no roles matched', async () => { + expect(acl.hasAny('Worker', 'updateAttributes', 'WRITE')).toBeFalsy(); + }); it('should return false if no roles matched', async () => { - expect(acl.hasAny('Worker', '*', 'WRITE')).toBeFalsy(); + expect(acl.hasAny('Worker', 'holidays', 'READ')).toBeTruthy(); + }); + + describe('*', () => { + it('should return true if an acl matched', async () => { + expect(acl.hasAny('Address', '*', 'WRITE')).toBeTruthy(); + }); + + it('should return false if no acls matched', async () => { + expect(acl.hasAny('Worker', '*', 'READ')).toBeFalsy(); + }); + }); + + describe('$authenticated', () => { + it('should return false if no acls matched', async () => { + expect(acl.hasAny('Url', 'getByUser', '*')).toBeFalsy(); + }); + + it('should return true if an acl matched', async () => { + expect(acl.hasAny('Url', 'getByUser', 'READ')).toBeTruthy(); + }); + }); + + describe('$everyone', () => { + it('should return false if no acls matched', async () => { + expect(acl.hasAny('TpvTransaction', 'start', 'READ')).toBeFalsy(); + }); + + it('should return false if an acl matched', async () => { + expect(acl.hasAny('TpvTransaction', 'start', 'WRITE')).toBeTruthy(); + }); }); }); }); From 992249c63886be023111d7eeeeacc42c5aabda85 Mon Sep 17 00:00:00 2001 From: jorgep Date: Mon, 10 Jun 2024 17:06:28 +0200 Subject: [PATCH 6/6] fix: refs #6273 redirect --- src/pages/Worker/WorkerCreate.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Worker/WorkerCreate.vue b/src/pages/Worker/WorkerCreate.vue index ebde15b72..5e81a3070 100644 --- a/src/pages/Worker/WorkerCreate.vue +++ b/src/pages/Worker/WorkerCreate.vue @@ -86,7 +86,7 @@ onBeforeMount(async () => { url-create="Workers/new" model="worker" :form-initial-data="formData" - @on-data-saved="({ id }) => $router.push({ path: `/worker/${id}` })" + @on-data-saved="(__, { id }) => $router.push({ path: `/worker/${id}` })" >