diff --git a/app/src/controllers/index.ts b/app/src/controllers/index.ts index e70dcde79..03ef8f16b 100644 --- a/app/src/controllers/index.ts +++ b/app/src/controllers/index.ts @@ -6,3 +6,4 @@ export { default as roadmapController } from './roadmap'; export { default as ssoController } from './sso'; export { default as submissionController } from './submission'; export { default as userController } from './user'; +export { default as yarsController } from './yars'; diff --git a/app/src/controllers/yars.ts b/app/src/controllers/yars.ts new file mode 100644 index 000000000..9b47389e8 --- /dev/null +++ b/app/src/controllers/yars.ts @@ -0,0 +1,22 @@ +import { yarsService } from '../services'; + +import type { NextFunction, Request, Response } from '../interfaces/IExpress'; + +const controller = { + getPermissions: async (req: Request, res: Response, next: NextFunction) => { + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const roles = await yarsService.getIdentityRoles((req.currentUser?.tokenPayload as any).preferred_username); + + const permissions = await Promise.all(roles.map((x) => yarsService.getRolePermissions(x.roleId))).then((x) => + x.flat() + ); + + res.status(200).json(permissions); + } catch (e: unknown) { + next(e); + } + } +}; + +export default controller; diff --git a/app/src/middleware/authorization.ts b/app/src/middleware/authorization.ts index db3fa4f20..6400a57a6 100644 --- a/app/src/middleware/authorization.ts +++ b/app/src/middleware/authorization.ts @@ -30,12 +30,20 @@ export const hasPermission = (resource: string, action: string) => { try { if (req.currentUser) { // eslint-disable-next-line @typescript-eslint/no-explicit-any - const roles = await yarsService.getIdentityRoles((req.currentUser?.tokenPayload as any).preferred_username); + const identityId = (req.currentUser?.tokenPayload as any).preferred_username; + + let roles = await yarsService.getIdentityRoles(identityId); + + // Auto assign PROPONENT if user has no roles + if (roles && roles.length === 0) { + await yarsService.assignRole(identityId, AccessRole.PROPONENT); + roles = await yarsService.getIdentityRoles(identityId); + } const userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, NIL), NIL); if (!userId) { - throw new Error('Invalid role user'); + throw new Error('Invalid user'); } // Permission/Scope checking for non developers diff --git a/app/src/routes/v1/index.ts b/app/src/routes/v1/index.ts index 697dee976..81d07704d 100644 --- a/app/src/routes/v1/index.ts +++ b/app/src/routes/v1/index.ts @@ -11,6 +11,8 @@ import roadmap from './roadmap'; import sso from './sso'; import submission from './submission'; import user from './user'; +import yars from './yars'; + import { Initiative } from '../../utils/enums/application'; const router = express.Router(); @@ -20,7 +22,7 @@ router.use(setInitiative(Initiative.HOUSING)); // Base v1 Responder router.get('/', (_req, res) => { res.status(200).json({ - endpoints: ['/document', '/enquiry', '/note', '/permit', '/roadmap', '/sso', '/submission', '/user'] + endpoints: ['/document', '/enquiry', '/note', '/permit', '/roadmap', '/sso', '/submission', '/user', '/yars'] }); }); @@ -32,5 +34,6 @@ router.use('/roadmap', roadmap); router.use('/sso', sso); router.use('/submission', submission); router.use('/user', user); +router.use('/yars', yars); export default router; diff --git a/app/src/routes/v1/yars.ts b/app/src/routes/v1/yars.ts new file mode 100644 index 000000000..268ae92e2 --- /dev/null +++ b/app/src/routes/v1/yars.ts @@ -0,0 +1,15 @@ +import express from 'express'; + +import { yarsController } from '../../controllers'; +import { requireSomeAuth } from '../../middleware/requireSomeAuth'; + +import type { NextFunction, Request, Response } from '../../interfaces/IExpress'; + +const router = express.Router(); +router.use(requireSomeAuth); + +router.get('/permissions', (req: Request, res: Response, next: NextFunction): void => { + yarsController.getPermissions(req, res, next); +}); + +export default router; diff --git a/app/src/services/yars.ts b/app/src/services/yars.ts index 0cc24e097..59b465fd9 100644 --- a/app/src/services/yars.ts +++ b/app/src/services/yars.ts @@ -1,9 +1,48 @@ /* eslint-disable no-useless-catch */ import prisma from '../db/dataConnection'; -import { Initiative } from '../utils/enums/application'; +import { AccessRole, Initiative } from '../utils/enums/application'; const service = { + assignRole: async (identityId: string, role: AccessRole, initiative?: Initiative) => { + try { + let roleResult; + + if (initiative) { + const i = await prisma.initiative.findFirstOrThrow({ + where: { + code: initiative + } + }); + + roleResult = await prisma.role.findFirstOrThrow({ + where: { + initiative_id: i.initiative_id, + user_type: role + } + }); + } else { + roleResult = await prisma.role.findFirstOrThrow({ + where: { + initiative_id: null, + user_type: role + } + }); + } + + const result = await prisma.identity_role.create({ + data: { + identity_id: identityId, + role_id: roleResult.role_id + } + }); + + return { identityId: result.identity_id, roleId: result.role_id }; + } catch (e: unknown) { + throw e; + } + }, + /** * @function getEnquiry * Gets roles for the specified identity @@ -55,6 +94,24 @@ const service = { } catch (e: unknown) { throw e; } + }, + + getRolePermissions: async (roleId: number) => { + try { + const result = await prisma.role_permission_vw.findMany({ + where: { + role_id: roleId + } + }); + + return result.map((x) => ({ + initiativeName: x.initiative_name, + resourceName: x.resource_name, + actionName: x.action_name + })); + } catch (e: unknown) { + throw e; + } } }; diff --git a/frontend/src/services/index.ts b/frontend/src/services/index.ts index 2af85a1f3..9055275d8 100644 --- a/frontend/src/services/index.ts +++ b/frontend/src/services/index.ts @@ -10,3 +10,4 @@ export { default as permitService } from './permitService'; export { default as roadmapService } from './roadmapService'; export { default as submissionService } from './submissionService'; export { default as userService } from './userService'; +export { default as yarsService } from './yarsService'; diff --git a/frontend/src/services/yarsService.ts b/frontend/src/services/yarsService.ts new file mode 100644 index 000000000..061aae943 --- /dev/null +++ b/frontend/src/services/yarsService.ts @@ -0,0 +1,11 @@ +import { appAxios } from './interceptors'; + +import type { AxiosResponse } from 'axios'; + +const PATH = 'yars'; + +export default { + getPermissions(): Promise { + return appAxios().get(`${PATH}/permissions`); + } +}; diff --git a/frontend/src/store/index.ts b/frontend/src/store/index.ts index 1850c0fdc..bf947b303 100644 --- a/frontend/src/store/index.ts +++ b/frontend/src/store/index.ts @@ -2,5 +2,6 @@ export { default as useAppStore } from './appStore'; export { default as useAuthStore } from './authStore'; export { default as useConfigStore } from './configStore'; export { default as useEnquiryStore } from './enquiryStore'; +export { default as usePermissionStore } from './permissionStore'; export { default as useSubmissionStore } from './submissionStore'; export { default as useTypeStore } from './typeStore'; diff --git a/frontend/src/store/permissionStore.ts b/frontend/src/store/permissionStore.ts new file mode 100644 index 000000000..74c267f29 --- /dev/null +++ b/frontend/src/store/permissionStore.ts @@ -0,0 +1,43 @@ +import { defineStore } from 'pinia'; +import { computed, readonly, ref } from 'vue'; + +import type { Ref } from 'vue'; + +export type PermissionStoreState = { + permissions: Ref>; +}; + +export const usePermissionStore = defineStore('permission', () => { + // State + const state: PermissionStoreState = { + permissions: ref([]) + }; + + // Getters + const getters = { + can: computed( + () => (initiative: string, resource: string, action: string) => + state.permissions.value.find( + (x) => x.initiative === initiative && x.resource === resource && x.action === action + ) + ) + }; + + // Actions + function setPermissions(data: any) { + state.permissions.value = data; + } + + return { + // State + state: readonly(state), + + // Getters + ...getters, + + // Actions + setPermissions + }; +}); + +export default usePermissionStore; diff --git a/frontend/src/views/oidc/OidcCallbackView.vue b/frontend/src/views/oidc/OidcCallbackView.vue index c03556e99..b6807af2a 100644 --- a/frontend/src/views/oidc/OidcCallbackView.vue +++ b/frontend/src/views/oidc/OidcCallbackView.vue @@ -3,8 +3,8 @@ import { onMounted } from 'vue'; import { useRouter } from 'vue-router'; import { Spinner } from '@/components/layout'; -import { PermissionService } from '@/services'; -import { useAuthStore } from '@/store'; +import { yarsService } from '@/services'; +import { useAuthStore, usePermissionStore } from '@/store'; import { StorageKey } from '@/utils/enums/application'; const authStore = useAuthStore(); @@ -13,10 +13,10 @@ const router = useRouter(); onMounted(async () => { await authStore.loginCallback(); - // Request basic access if the logged in user has no roles - if (!authStore.getClientRoles || !authStore.getClientRoles.length) { - await new PermissionService().requestBasicAccess(); - } + // Get front end permissions + const permissions = await yarsService.getPermissions(); + console.log(permissions); + usePermissionStore().setPermissions(permissions.data); // Return user back to original login entrypoint if specified const entrypoint = window.sessionStorage.getItem(StorageKey.AUTH);