From f684e121365b4127761b1141d74867c8726f6251 Mon Sep 17 00:00:00 2001 From: Siyasanga Mtshokotsha Date: Thu, 5 Dec 2024 12:10:15 +0200 Subject: [PATCH] fix: Stops local sys admin from creating national level staff (#8112) * Move access mgnt into the gateway service It is better to have in the gateway since most of access mgnt is handled there already https://github.com/opencrvs/opencrvs-core/issues/7698 * refactor: the getSystemRoles() to propery use filters The way we were building the criteria object was buggy especially for when we are filtering based on user roles https://github.com/opencrvs/opencrvs-core/issues/7698 * Filter User roles based on user that's requesting This is avoid users with lower roles creating or updating other users with higher roles https://github.com/opencrvs/opencrvs-core/issues/7698 * Record changes in the CHANGELOG https://github.com/opencrvs/opencrvs-core/issues/7698 * Revert "Filter User roles based on user that's requesting" This reverts commit b46c67ea974e94fc4956df973cdd9b6974d3d4a9. * Revert "refactor: the getSystemRoles() to propery use filters" This reverts commit fb400bd1141363f06c9b942157f2a6ba5d2f6dc5. * Revert "Move access mgnt into the gateway service" This reverts commit a9c6fa8e44ded436f8545f6d8dd9ffe55adeda9a. * Fix failing Role feature's resolver tests https://github.com/opencrvs/opencrvs-core/issues/7698 * Stop sys admins from de-activating themselves The sys admin will no longer see the feature for their own accounts, it will only available on other users, this should stop them from eccidentally deactivating their accounts. https://github.com/opencrvs/opencrvs-core/issues/7691 * Minor tisy up --------- Co-authored-by: euanmillar --- CHANGELOG.md | 1 + .../src/views/SysAdmin/Team/user/UserList.tsx | 13 ++-- .../client/src/views/SysAdmin/Team/utils.ts | 5 ++ .../client/src/views/UserAudit/UserAudit.tsx | 11 +++- .../src/features/role/root-resolvers.test.ts | 31 ++++++++- .../src/features/role/root-resolvers.ts | 16 ++++- packages/gateway/src/features/role/utils.ts | 65 +++++++++++++++++++ .../src/features/user/root-resolvers.ts | 12 +++- .../gateway/src/features/user/utils/index.ts | 18 +++++ 9 files changed, 160 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b2d038c8a8..d77877e1af1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Bug fixes - Maximum upload file size limit is now based on the size of the uploaded files after compression and not before. [#7840](https://github.com/opencrvs/opencrvs-core/issues/7840) +- Stops local sys admins creating national level users. [#7698](https://github.com/opencrvs/opencrvs-core/issues/7698) ### New features diff --git a/packages/client/src/views/SysAdmin/Team/user/UserList.tsx b/packages/client/src/views/SysAdmin/Team/user/UserList.tsx index 34a7659c9ab..e48985f71b1 100644 --- a/packages/client/src/views/SysAdmin/Team/user/UserList.tsx +++ b/packages/client/src/views/SysAdmin/Team/user/UserList.tsx @@ -39,7 +39,8 @@ import { SysAdminContentWrapper } from '@client/views/SysAdmin/SysAdminContentWr import { getAddressName, getUserRoleIntlKey, - UserStatus + UserStatus, + canDeactivateUser } from '@client/views/SysAdmin/Team/utils' import { LinkButton } from '@opencrvs/components/lib/buttons' import { Button } from '@opencrvs/components/lib/Button' @@ -396,7 +397,7 @@ function UserListComponent(props: IProps) { ) const getMenuItems = useCallback( - function getMenuItems(user: User) { + function getMenuItems(user: User, userDetails: UserDetails | null) { const menuItems = [ { label: intl.formatMessage(messages.editUserDetailsTitle), @@ -432,7 +433,11 @@ function UserListComponent(props: IProps) { }) } - if (user.status === 'active') { + if ( + userDetails && + user.status === 'active' && + canDeactivateUser(user.id, userDetails) + ) { menuItems.push({ label: intl.formatMessage(messages.deactivate), handler: () => toggleUserActivationModal(user) @@ -530,7 +535,7 @@ function UserListComponent(props: IProps) { toggleButton={ } - menuItems={getMenuItems(user)} + menuItems={getMenuItems(user, userDetails)} /> )} diff --git a/packages/client/src/views/SysAdmin/Team/utils.ts b/packages/client/src/views/SysAdmin/Team/utils.ts index 19f647bb920..c02e5ac0556 100644 --- a/packages/client/src/views/SysAdmin/Team/utils.ts +++ b/packages/client/src/views/SysAdmin/Team/utils.ts @@ -13,6 +13,7 @@ import { IntlShape, MessageDescriptor } from 'react-intl' import { messages } from '@client/i18n/messages/views/userSetup' import { SystemRoleType } from '@client/utils/gateway' import { ILocation, IOfflineData } from '@client/offline/reducer' +import { UserDetails } from '@client/utils/userUtils' export enum UserStatus { ACTIVE, @@ -112,3 +113,7 @@ export function getUserSystemRole( export const getUserRoleIntlKey = (_roleId: string) => { return `role.${_roleId}` } + +export const canDeactivateUser = (id: string, userDetails: UserDetails) => { + return id !== userDetails.id ? true : false +} diff --git a/packages/client/src/views/UserAudit/UserAudit.tsx b/packages/client/src/views/UserAudit/UserAudit.tsx index 680ed4cec5d..fcf6fb32d8c 100644 --- a/packages/client/src/views/UserAudit/UserAudit.tsx +++ b/packages/client/src/views/UserAudit/UserAudit.tsx @@ -23,7 +23,10 @@ import { AvatarSmall } from '@client/components/Avatar' import styled from 'styled-components' import { ToggleMenu } from '@opencrvs/components/lib/ToggleMenu' import { Button } from '@opencrvs/components/lib/Button' -import { getUserRoleIntlKey } from '@client/views/SysAdmin//Team/utils' +import { + getUserRoleIntlKey, + canDeactivateUser +} from '@client/views/SysAdmin/Team/utils' import { EMPTY_STRING, LANG_EN } from '@client/utils/constants' import { Loader } from '@opencrvs/components/lib/Loader' import { messages as userSetupMessages } from '@client/i18n/messages/views/userSetup' @@ -246,7 +249,11 @@ export const UserAudit = () => { ) } - if (status === 'active') { + if ( + status === 'active' && + userDetails && + canDeactivateUser(userId, userDetails) + ) { menuItems.push({ label: intl.formatMessage(sysMessages.deactivate), handler: () => toggleUserActivationModal() diff --git a/packages/gateway/src/features/role/root-resolvers.test.ts b/packages/gateway/src/features/role/root-resolvers.test.ts index fec0be47d08..c4de70bd8e7 100644 --- a/packages/gateway/src/features/role/root-resolvers.test.ts +++ b/packages/gateway/src/features/role/root-resolvers.test.ts @@ -202,17 +202,42 @@ describe('Role root resolvers', () => { } ] it('returns full role list', async () => { + const sysAdminToken = jwt.sign( + { scope: ['natlsysadmin'] }, + readFileSync('./test/cert.key'), + { + subject: 'ba7022f0ff4822', + algorithm: 'RS256', + issuer: 'opencrvs:auth-service', + audience: 'opencrvs:gateway-user' + } + ) + const authHeaderSysAdmin = { + Authorization: `Bearer ${sysAdminToken}` + } fetch.mockResponseOnce(JSON.stringify(dummyRoleList)) - const response = await resolvers.Query!.getSystemRoles( {}, {}, - { headers: undefined } + { headers: authHeaderSysAdmin } ) expect(response).toEqual(dummyRoleList) }) it('returns filtered role list', async () => { + const sysAdminToken = jwt.sign( + { scope: ['sysadmin'] }, + readFileSync('./test/cert.key'), + { + subject: 'ba7022f0ff4822', + algorithm: 'RS256', + issuer: 'opencrvs:auth-service', + audience: 'opencrvs:gateway-user' + } + ) + const authHeaderSysAdmin = { + Authorization: `Bearer ${sysAdminToken}` + } fetch.mockResponseOnce(JSON.stringify([dummyRoleList[2]])) const response = await resolvers.Query!.getSystemRoles( @@ -225,7 +250,7 @@ describe('Role root resolvers', () => { type: 'Mayor', active: true }, - { headers: undefined } + { headers: authHeaderSysAdmin } ) expect(response).toEqual([dummyRoleList[2]]) }) diff --git a/packages/gateway/src/features/role/root-resolvers.ts b/packages/gateway/src/features/role/root-resolvers.ts index efe34779f16..b4d3152d690 100644 --- a/packages/gateway/src/features/role/root-resolvers.ts +++ b/packages/gateway/src/features/role/root-resolvers.ts @@ -12,8 +12,13 @@ import { GQLResolver } from '@gateway/graphql/schema' import fetch from '@gateway/fetch' import { USER_MANAGEMENT_URL } from '@gateway/constants' import { IRoleSearchPayload } from '@gateway/features/role/type-resolvers' -import { transformMongoComparisonObject } from '@gateway/features/role/utils' +import { + getAccessibleRolesForScope, + SystemRole, + transformMongoComparisonObject +} from '@gateway/features/role/utils' import { hasScope } from '@gateway/features/user/utils' +import { getTokenPayload } from '@opencrvs/commons/authentication' export const resolvers: GQLResolver = { Query: { @@ -51,6 +56,7 @@ export const resolvers: GQLResolver = { if (active !== null) { payload = { ...payload, active } } + const res = await fetch(`${USER_MANAGEMENT_URL}getSystemRoles`, { method: 'POST', body: JSON.stringify(payload), @@ -59,7 +65,13 @@ export const resolvers: GQLResolver = { ...authHeader } }) - return await res.json() + + const { scope } = getTokenPayload(authHeader.Authorization.split(' ')[1]) + const accessibleSysAdminRoles = getAccessibleRolesForScope(scope) + const allSysAdminRoles = (await res.json()) as SystemRole[] + return allSysAdminRoles.filter((sysAdminRole) => + accessibleSysAdminRoles?.includes(sysAdminRole.value) + ) } }, Mutation: { diff --git a/packages/gateway/src/features/role/utils.ts b/packages/gateway/src/features/role/utils.ts index 7a9448f3af3..e3cec0da12c 100644 --- a/packages/gateway/src/features/role/utils.ts +++ b/packages/gateway/src/features/role/utils.ts @@ -8,6 +8,58 @@ * * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ + +import { Scope } from '@opencrvs/commons/authentication' + +export const SYSTEM_ROLE_KEYS = [ + 'FIELD_AGENT', + 'LOCAL_REGISTRAR', + 'LOCAL_SYSTEM_ADMIN', + 'NATIONAL_REGISTRAR', + 'NATIONAL_SYSTEM_ADMIN', + 'PERFORMANCE_MANAGEMENT', + 'REGISTRATION_AGENT' +] as const + +// Derive the type from SYSTEM_ROLE_KEYS +type SystemRoleKeyType = (typeof SYSTEM_ROLE_KEYS)[number] + +export const SysAdminAccessMap: Partial< + Record +> = { + LOCAL_SYSTEM_ADMIN: [ + 'FIELD_AGENT', + 'LOCAL_REGISTRAR', + 'LOCAL_SYSTEM_ADMIN', + 'PERFORMANCE_MANAGEMENT', + 'REGISTRATION_AGENT' + ], + NATIONAL_SYSTEM_ADMIN: [ + 'FIELD_AGENT', + 'LOCAL_REGISTRAR', + 'LOCAL_SYSTEM_ADMIN', + 'NATIONAL_REGISTRAR', + 'NATIONAL_SYSTEM_ADMIN', + 'PERFORMANCE_MANAGEMENT', + 'REGISTRATION_AGENT' + ] +} + +type UserRole = { + labels: Label[] +} + +type Label = { + lang: string + label: string +} + +export type SystemRole = { + value: SystemRoleKeyType + roles: UserRole[] + active: boolean + creationDate: number +} export interface IComparisonObject { eq?: string gt?: string @@ -46,3 +98,16 @@ export function transformMongoComparisonObject( {} ) } + +export function getAccessibleRolesForScope(scope: Scope[]) { + let roleFilter: keyof typeof SysAdminAccessMap + if (scope.includes('natlsysadmin')) { + roleFilter = 'NATIONAL_SYSTEM_ADMIN' + } else if (scope.includes('sysadmin')) { + roleFilter = 'LOCAL_SYSTEM_ADMIN' + } else { + throw Error('Create user is only allowed for sysadmin/natlsysadmin') + } + + return SysAdminAccessMap[roleFilter] +} diff --git a/packages/gateway/src/features/user/root-resolvers.ts b/packages/gateway/src/features/user/root-resolvers.ts index 08497c6b00a..1b38d9eadf0 100644 --- a/packages/gateway/src/features/user/root-resolvers.ts +++ b/packages/gateway/src/features/user/root-resolvers.ts @@ -20,7 +20,8 @@ import { hasScope, inScope, isTokenOwner, - getUserId + getUserId, + canAssignRole } from '@gateway/features/user/utils' import { GQLHumanNameInput, @@ -37,6 +38,7 @@ import { validateAttachments } from '@gateway/utils/validators' import { postMetrics } from '@gateway/features/metrics/service' import { uploadBase64ToMinio } from '@gateway/features/documents/service' import { rateLimitedResolver } from '@gateway/rate-limit' +import { getTokenPayload } from '@opencrvs/commons/authentication' export const resolvers: GQLResolver = { Query: { @@ -272,6 +274,14 @@ export const resolvers: GQLResolver = { ) } + const { scope: loggedInUserScope } = getTokenPayload( + authHeader.Authorization.split(' ')[1] + ) + + if (!canAssignRole(loggedInUserScope, user)) { + throw Error('Create user is only allowed for sysadmin/natlsysadmin') + } + try { if (user.signature) { await validateAttachments([user.signature]) diff --git a/packages/gateway/src/features/user/utils/index.ts b/packages/gateway/src/features/user/utils/index.ts index 9618fe3c29a..bbe9e1c70ca 100644 --- a/packages/gateway/src/features/user/utils/index.ts +++ b/packages/gateway/src/features/user/utils/index.ts @@ -17,6 +17,8 @@ import { import * as decode from 'jwt-decode' import fetch from '@gateway/fetch' import { Scope } from '@opencrvs/commons/authentication' +import { GQLUserInput } from '@gateway/graphql/schema' +import { SysAdminAccessMap } from '@gateway/features/role/utils' export interface ITokenPayload { sub: string @@ -49,6 +51,22 @@ export async function getUser( return await res.json() } +export function canAssignRole( + loggedInUserScope: Scope[], + userToSave: GQLUserInput +) { + let roleFilter: keyof typeof SysAdminAccessMap + if (loggedInUserScope.includes('natlsysadmin')) { + roleFilter = 'NATIONAL_SYSTEM_ADMIN' + } else if (loggedInUserScope.includes('sysadmin')) { + roleFilter = 'LOCAL_SYSTEM_ADMIN' + } else { + throw Error('Create user is only allowed for sysadmin/natlsysadmin') + } + + return SysAdminAccessMap[roleFilter]?.includes(userToSave.systemRole) +} + export async function getSystem( body: { [key: string]: string | undefined }, authHeader: IAuthHeader