diff --git a/src/composables/useAcl.js b/src/composables/useAcl.js new file mode 100644 index 000000000..46aaa3c25 --- /dev/null +++ b/src/composables/useAcl.js @@ -0,0 +1,33 @@ +import axios from 'axios'; +import { useState } from './useState'; + +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, prop, accessType) { + const acls = state.getAcls().value[model]; + if (acls) + return ['*', prop].some((key) => { + const acl = acls[key]; + return acl && (acl['*'] || acl[accessType]); + }); + } + + 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 f20209494..c2ac1740c 100644 --- a/src/composables/useState.js +++ b/src/composables/useState.js @@ -15,6 +15,7 @@ if (sessionStorage.getItem('user')) user.value = JSON.parse(sessionStorage.getItem('user')); const roles = ref([]); +const acls = ref([]); const tokenConfig = ref({}); const drawer = ref(true); const headerMounted = ref(false); @@ -42,6 +43,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; @@ -69,6 +78,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(); } diff --git a/test/vitest/__tests__/composables/useAcl.spec.js b/test/vitest/__tests__/composables/useAcl.spec.js new file mode 100644 index 000000000..a2b44b5e7 --- /dev/null +++ b/test/vitest/__tests__/composables/useAcl.spec.js @@ -0,0 +1,88 @@ +import { vi, describe, expect, it, beforeAll, afterAll } 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', + }, + { + model: 'Worker', + property: 'holidays', + accessType: 'READ', + permission: 'ALLOW', + principalType: 'ROLE', + principalId: 'employee', + }, + { + model: 'Url', + property: 'getByUser', + accessType: 'READ', + permission: 'ALLOW', + principalType: 'ROLE', + principalId: '$everyone', + }, + { + model: 'TpvTransaction', + property: 'start', + accessType: 'WRITE', + permission: 'ALLOW', + principalType: 'ROLE', + principalId: '$authenticated', + }, + ]; + + beforeAll(async () => { + vi.spyOn(axios, 'get').mockResolvedValue({ data: mockAcls }); + await acl.fetch(); + }); + + afterAll(async () => await flushPromises()); + + describe('hasAny', () => { + 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', '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(); + }); + }); + }); +}); diff --git a/test/vitest/__tests__/composables/useSession.spec.js b/test/vitest/__tests__/composables/useSession.spec.js index 2292859a9..831acbf18 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'; @@ -87,13 +87,17 @@ 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 +121,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');