Skip to content

Commit

Permalink
fix: Stops local sys admin from creating national level staff (#8112)
Browse files Browse the repository at this point in the history
* Move access mgnt into the gateway service

It is better to have in the gateway since most of access mgnt is handled there already

#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

#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

#7698

* Record changes in the CHANGELOG

#7698

* Revert "Filter User roles based on user that's requesting"

This reverts commit b46c67e.

* Revert "refactor: the getSystemRoles() to propery use filters"

This reverts commit fb400bd.

* Revert "Move access mgnt into the gateway service"

This reverts commit a9c6fa8.

* Fix failing Role feature's resolver tests

#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.

#7691

* Minor tisy up

---------

Co-authored-by: euanmillar <euanmillar77@gmail.com>
  • Loading branch information
Siyasanga and euanmillar authored Dec 5, 2024
1 parent ef1308d commit f684e12
Show file tree
Hide file tree
Showing 9 changed files with 160 additions and 12 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 9 additions & 4 deletions packages/client/src/views/SysAdmin/Team/user/UserList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -530,7 +535,7 @@ function UserListComponent(props: IProps) {
toggleButton={
<Icon name="DotsThreeVertical" color="primary" size="large" />
}
menuItems={getMenuItems(user)}
menuItems={getMenuItems(user, userDetails)}
/>
)}
</Stack>
Expand Down
5 changes: 5 additions & 0 deletions packages/client/src/views/SysAdmin/Team/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
11 changes: 9 additions & 2 deletions packages/client/src/views/UserAudit/UserAudit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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()
Expand Down
31 changes: 28 additions & 3 deletions packages/gateway/src/features/role/root-resolvers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -225,7 +250,7 @@ describe('Role root resolvers', () => {
type: 'Mayor',
active: true
},
{ headers: undefined }
{ headers: authHeaderSysAdmin }
)
expect(response).toEqual([dummyRoleList[2]])
})
Expand Down
16 changes: 14 additions & 2 deletions packages/gateway/src/features/role/root-resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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),
Expand All @@ -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: {
Expand Down
65 changes: 65 additions & 0 deletions packages/gateway/src/features/role/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<SystemRoleKeyType, SystemRoleKeyType[]>
> = {
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
Expand Down Expand Up @@ -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]
}
12 changes: 11 additions & 1 deletion packages/gateway/src/features/user/root-resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import {
hasScope,
inScope,
isTokenOwner,
getUserId
getUserId,
canAssignRole
} from '@gateway/features/user/utils'
import {
GQLHumanNameInput,
Expand All @@ -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: {
Expand Down Expand Up @@ -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])
Expand Down
18 changes: 18 additions & 0 deletions packages/gateway/src/features/user/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit f684e12

Please sign in to comment.