Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Stops local sys admin from creating national level staff #8112

Merged
merged 10 commits into from
Dec 5, 2024
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
Loading