From 4c899508257ae4c67be9e9d51e172d5629905965 Mon Sep 17 00:00:00 2001 From: Dylan Barkowsky <37922247+dbarkowsky@users.noreply.github.com> Date: Wed, 18 Sep 2024 14:10:26 -0700 Subject: [PATCH] PIMS-2081 Frontend Authorization Changes (#2685) Co-authored-by: LawrenceLau2020 <68400651+LawrenceLau2020@users.noreply.github.com> --- react-app/src/App.tsx | 2 +- react-app/src/components/layout/Header.tsx | 6 +++--- .../components/map/controls/FilterControl.tsx | 5 ++--- .../map/parcelPopup/ParcelPopup.tsx | 6 +++--- .../src/components/projects/ProjectDetail.tsx | 6 +++--- .../src/components/projects/ProjectDialog.tsx | 4 ++-- .../src/components/projects/ProjectForms.tsx | 4 ++-- .../components/property/PropertyDetail.tsx | 4 ++-- react-app/src/components/table/DataTable.tsx | 4 ++-- react-app/src/components/users/UserDetail.tsx | 4 ++-- react-app/src/constants/roles.ts | 8 ++++---- react-app/src/guards/AuthRouteGuard.tsx | 5 +---- react-app/src/hooks/api/useUserAgencies.ts | 2 +- react-app/src/hooks/usePimsUser.ts | 19 +++++++++++++++++++ react-app/src/pages/AccessRequest.tsx | 6 +++--- 15 files changed, 50 insertions(+), 35 deletions(-) diff --git a/react-app/src/App.tsx b/react-app/src/App.tsx index a2db5b3e10..1d98bd524f 100644 --- a/react-app/src/App.tsx +++ b/react-app/src/App.tsx @@ -66,7 +66,7 @@ const Router = () => { element={ auth.keycloak.isAuthenticated && auth.pimsUser.data?.Status === 'Active' && - auth.keycloak.user?.client_roles?.length ? ( + auth.pimsUser.data?.RoleId ? ( showMap() ) : ( diff --git a/react-app/src/components/layout/Header.tsx b/react-app/src/components/layout/Header.tsx index b3999ad91c..d597df78bd 100644 --- a/react-app/src/components/layout/Header.tsx +++ b/react-app/src/components/layout/Header.tsx @@ -62,7 +62,7 @@ const AppBrand = () => { const Header: React.FC = () => { const auth = useContext(AuthContext); - const { logout, isAuthenticated, login, user } = useSSO(); + const { logout, isAuthenticated, login } = useSSO(); const theme = useTheme(); const navigate = useNavigate(); @@ -105,9 +105,9 @@ const Header: React.FC = () => { {isAuthenticated && auth?.pimsUser?.data?.Status === 'Active' && - auth.keycloak.user?.client_roles?.length && ( + auth?.pimsUser?.data?.RoleId && ( <> - {user.client_roles?.includes(Roles.ADMIN) ? ( + {auth.pimsUser.hasOneOfRoles([Roles.ADMIN]) ? ( <> { options={ agencyOptions?.filter( (option) => - user.keycloak.hasRoles([Roles.ADMIN, Roles.AUDITOR], { - requireAllRoles: false, - }) || usersAgenciesData?.includes(option.value), + user.pimsUser?.hasOneOfRoles([Roles.ADMIN, Roles.AUDITOR]) || + usersAgenciesData?.includes(option.value), ) ?? [] } allowNestedIndent diff --git a/react-app/src/components/map/parcelPopup/ParcelPopup.tsx b/react-app/src/components/map/parcelPopup/ParcelPopup.tsx index ccbe41181e..df3dfe4f86 100644 --- a/react-app/src/components/map/parcelPopup/ParcelPopup.tsx +++ b/react-app/src/components/map/parcelPopup/ParcelPopup.tsx @@ -38,7 +38,7 @@ export const ParcelPopup = (props: ParcelPopupProps) => { const [tabValue, setTabValue] = useState('0'); const { size = 'large', scrollOnClick } = props; - const { keycloak } = useContext(AuthContext); + const { pimsUser } = useContext(AuthContext); const { data: ltsaData, @@ -65,7 +65,7 @@ export const ParcelPopup = (props: ParcelPopupProps) => { useEffect(() => { if (parcelData && clickPosition) { refreshLtsa(); - if (keycloak.hasRoles([Roles.ADMIN])) { + if (pimsUser.hasOneOfRoles([Roles.ADMIN])) { refreshBCA(); } } @@ -148,7 +148,7 @@ export const ParcelPopup = (props: ParcelPopupProps) => { > - {keycloak.hasRoles([Roles.ADMIN]) ? : <>} + {pimsUser.hasOneOfRoles([Roles.ADMIN]) ? : <>} diff --git a/react-app/src/components/projects/ProjectDetail.tsx b/react-app/src/components/projects/ProjectDetail.tsx index 761f5ceae0..bac5094029 100644 --- a/react-app/src/components/projects/ProjectDetail.tsx +++ b/react-app/src/components/projects/ProjectDetail.tsx @@ -71,7 +71,7 @@ interface ProjectInfo extends Project { const ProjectDetail = (props: IProjectDetail) => { const navigate = useNavigate(); const { id } = useParams(); - const { keycloak } = useContext(AuthContext); + const { pimsUser } = useContext(AuthContext); const lookup = useContext(LookupContext); const api = usePimsApi(); const { data: lookupData, getLookupValueById } = useContext(LookupContext); @@ -88,8 +88,8 @@ const ProjectDetail = (props: IProjectDetail) => { } }, [data]); - const isAuditor = keycloak.hasRoles([Roles.AUDITOR]); - const isAdmin = keycloak.hasRoles([Roles.ADMIN]); + const isAuditor = pimsUser.hasOneOfRoles([Roles.AUDITOR]); + const isAdmin = pimsUser.hasOneOfRoles([Roles.ADMIN]); const { submit: deleteProject, submitting: deletingProject } = useDataSubmitter( api.projects.deleteProjectById, diff --git a/react-app/src/components/projects/ProjectDialog.tsx b/react-app/src/components/projects/ProjectDialog.tsx index f07fbe6464..4ea951be51 100644 --- a/react-app/src/components/projects/ProjectDialog.tsx +++ b/react-app/src/components/projects/ProjectDialog.tsx @@ -43,7 +43,7 @@ export const ProjectGeneralInfoDialog = (props: IProjectGeneralInfoDialog) => { const { open, postSubmit, onCancel, initialValues } = props; const api = usePimsApi(); const { data: lookupData } = useContext(LookupContext); - const { keycloak } = useContext(AuthContext); + const { pimsUser } = useContext(AuthContext); const { submit, submitting } = useDataSubmitter(api.projects.updateProject); const [approvedStatus, setApprovedStatus] = useState(null); const projectFormMethods = useForm({ @@ -167,7 +167,7 @@ export const ProjectGeneralInfoDialog = (props: IProjectGeneralInfoDialog) => { const status = projectFormMethods.watch('StatusId'); const requireNotificationAcknowledge = approvedStatus == status && status !== initialValues?.StatusId; - const isAdmin = keycloak.hasRoles([Roles.ADMIN]); + const isAdmin = pimsUser.hasOneOfRoles([Roles.ADMIN]); console.log('project form values', projectFormMethods.getValues()); return ( { const { data: lookupData } = useContext(LookupContext); - const { keycloak } = useContext(AuthContext); - const canEdit = keycloak.hasRoles([Roles.ADMIN]); + const { pimsUser } = useContext(AuthContext); + const canEdit = pimsUser.hasOneOfRoles([Roles.ADMIN]); return ( diff --git a/react-app/src/components/property/PropertyDetail.tsx b/react-app/src/components/property/PropertyDetail.tsx index ac0f170fd8..3e8877ab9a 100644 --- a/react-app/src/components/property/PropertyDetail.tsx +++ b/react-app/src/components/property/PropertyDetail.tsx @@ -40,7 +40,7 @@ interface IPropertyDetail { const PropertyDetail = (props: IPropertyDetail) => { const navigate = useNavigate(); const params = useParams(); - const { keycloak } = useContext(AuthContext); + const { pimsUser } = useContext(AuthContext); const { getLookupValueById } = useContext(LookupContext); const parcelId = isNaN(Number(params.parcelId)) ? null : Number(params.parcelId); const buildingId = isNaN(Number(params.buildingId)) ? null : Number(params.buildingId); @@ -100,7 +100,7 @@ const PropertyDetail = (props: IPropertyDetail) => { const userAgencyIds = userAgencies.map((a) => a.Id); const canEdit = - keycloak.hasRoles([Roles.ADMIN]) || + pimsUser.hasOneOfRoles([Roles.ADMIN]) || userAgencyIds.includes(parcel?.parsedBody?.AgencyId) || userAgencyIds.includes(building?.parsedBody?.AgencyId); diff --git a/react-app/src/components/table/DataTable.tsx b/react-app/src/components/table/DataTable.tsx index 9a7315810d..2f73e1dcb8 100644 --- a/react-app/src/components/table/DataTable.tsx +++ b/react-app/src/components/table/DataTable.tsx @@ -436,8 +436,8 @@ export const FilterSearchDataGrid = (props: FilterSearchDataGridProps) => { : `(${props.rowCountProp ?? 0} rows)`; }, [props.tableOperationMode, rowCount, props.rowCountProp]); - const { keycloak } = useContext(AuthContext); - const isAuditor = keycloak.hasRoles([Roles.AUDITOR]); + const { pimsUser } = useContext(AuthContext); + const isAuditor = pimsUser.hasOneOfRoles([Roles.AUDITOR]); return ( <> diff --git a/react-app/src/components/users/UserDetail.tsx b/react-app/src/components/users/UserDetail.tsx index 1ddc08aa75..d1bc9be109 100644 --- a/react-app/src/components/users/UserDetail.tsx +++ b/react-app/src/components/users/UserDetail.tsx @@ -29,7 +29,7 @@ interface UserProfile extends User { const UserDetail = ({ onClose }: IUserDetail) => { const { id } = useParams(); - const { keycloak, pimsUser } = useContext(AuthContext); + const { pimsUser } = useContext(AuthContext); const { data: lookupData, getLookupValueById } = useContext(LookupContext); const api = usePimsApi(); @@ -100,7 +100,7 @@ const UserDetail = ({ onClose }: IUserDetail) => { mode: 'onBlur', }); - const canEdit = keycloak.hasRoles([Roles.ADMIN]); + const canEdit = pimsUser.hasOneOfRoles([Roles.ADMIN]); useEffect(() => { refreshData(); diff --git a/react-app/src/constants/roles.ts b/react-app/src/constants/roles.ts index f618938cc6..fe9e58d7aa 100644 --- a/react-app/src/constants/roles.ts +++ b/react-app/src/constants/roles.ts @@ -1,11 +1,11 @@ /** * @enum - * These must match the role names in Keycloak exactly. + * The values in this enum must exactly mirror the IDs in the Role table. */ export enum Roles { - ADMIN = 'Administrator', - GENERAL_USER = 'General User', - AUDITOR = 'Auditor', + ADMIN = '00000000-0000-0000-0000-000000000000', + GENERAL_USER = '00000000-0000-0000-0000-000000000001', + AUDITOR = '00000000-0000-0000-0000-000000000002', } export interface KeycloakRole { diff --git a/react-app/src/guards/AuthRouteGuard.tsx b/react-app/src/guards/AuthRouteGuard.tsx index 3e2fa6aaa4..365370ac32 100644 --- a/react-app/src/guards/AuthRouteGuard.tsx +++ b/react-app/src/guards/AuthRouteGuard.tsx @@ -30,10 +30,7 @@ const AuthRouteGuard = (props: AuthGuardProps) => { if (authStateContext.pimsUser?.data) { // Redirect from page if lacking roles - if ( - permittedRoles && - !authStateContext.keycloak.hasRoles(permittedRoles, { requireAllRoles: false }) - ) { + if (permittedRoles && !authStateContext.pimsUser?.hasOneOfRoles(permittedRoles)) { navigate(redirectRoute ?? '/'); } // Redirect from page if user does not have Active status diff --git a/react-app/src/hooks/api/useUserAgencies.ts b/react-app/src/hooks/api/useUserAgencies.ts index 9f68519977..b6aec79fc2 100644 --- a/react-app/src/hooks/api/useUserAgencies.ts +++ b/react-app/src/hooks/api/useUserAgencies.ts @@ -10,7 +10,7 @@ const useUserAgencies = () => { const userContext = useContext(AuthContext); const { ungroupedAgencies, agencyOptions } = useGroupedAgenciesApi(); const api = usePimsApi(); - const isAdmin = userContext.keycloak.hasRoles([Roles.ADMIN]); + const isAdmin = userContext.pimsUser?.hasOneOfRoles([Roles.ADMIN]); const { data: userAgencies, refreshData: refreshUserAgencies } = useDataLoader(() => api.users.getUsersAgencyIds(userContext.keycloak.user.preferred_username), ); diff --git a/react-app/src/hooks/usePimsUser.ts b/react-app/src/hooks/usePimsUser.ts index fcc87b6371..c83362edd2 100644 --- a/react-app/src/hooks/usePimsUser.ts +++ b/react-app/src/hooks/usePimsUser.ts @@ -2,13 +2,21 @@ import { useSSO } from '@bcgov/citz-imb-sso-react'; import usePimsApi from './usePimsApi'; import useDataLoader from './useDataLoader'; import { User } from './api/useUsersApi'; +import { Roles } from '@/constants/roles'; export interface IPimsUser { data?: User; refreshData: () => Promise; isLoading: boolean; + hasOneOfRoles: (requiredRoles: Roles[]) => boolean; } +/** + * A hook that retrieves the active user's info from the database. + * It uses the username provided by Keycloak to find the corresponding user. + * It includes a function `hasOneOfRoles` to allow for role-based permissions checks. + * @returns Object containing the user's database record and functions related to the loading of this data. + */ const usePimsUser = () => { const keycloak = useSSO(); const api = usePimsApi(); @@ -18,10 +26,21 @@ const usePimsUser = () => { loadOnce(); } + /** + * Checks the user's roles and returns a boolean if they have the given required roles. + * @param {Roles[]} requiredRoles An array of required Roles. + * @returns True|False depending on user role and required roles. + */ + const hasOneOfRoles = (requiredRoles: Roles[]): boolean => { + if (!data || !data.RoleId || !requiredRoles || !requiredRoles.length) return false; + return requiredRoles.includes(data.RoleId as Roles); + }; + return { data, refreshData, isLoading, + hasOneOfRoles, }; }; diff --git a/react-app/src/pages/AccessRequest.tsx b/react-app/src/pages/AccessRequest.tsx index 965dd2f149..8678c3e24b 100644 --- a/react-app/src/pages/AccessRequest.tsx +++ b/react-app/src/pages/AccessRequest.tsx @@ -152,14 +152,14 @@ export const AccessRequest = () => { if ( auth.pimsUser?.data?.Status && - auth.pimsUser.data?.Status === 'Active' && - auth.keycloak.user?.client_roles?.length + auth.pimsUser?.data?.Status === 'Active' && + auth.pimsUser?.data?.RoleId ) { return ; } const selectPageContent = () => { - if (auth.pimsUser.data?.Status === 'Active' && !auth.keycloak.user?.client_roles?.length) { + if (auth.pimsUser.data?.Status === 'Active' && !auth.pimsUser.data?.RoleId) { return ( <>