Skip to content

Commit

Permalink
Merge pull request #7923 from opencrvs/validate-scopes-before-seeding
Browse files Browse the repository at this point in the history
feat(user-scopes): validate scopes before seeding
  • Loading branch information
Zangetsu101 authored Nov 11, 2024
2 parents b27226f + b2f1627 commit fc1b3d2
Show file tree
Hide file tree
Showing 9 changed files with 98 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export default async function authenticateSuperUserHandler(

const SUPER_ADMIN_SCOPES = [
SCOPES.BYPASSRATELIMIT,
SCOPES.CONFIG_UPDATE_ALL
SCOPES.USER_DATA_SEEDING
] satisfies Scope[]

const token = await createToken(
Expand Down
12 changes: 8 additions & 4 deletions packages/commons/src/scopes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ export const SCOPES = {
RECORD_UNASSIGN_OTHERS: 'record.unassign-others',

// validate
RECORD_SUBMIT_FOR_APPROVAL: 'record.submit-for-approval',
RECORD_SUBMIT_FOR_UPDATES: 'record.submit-for-updates',
RECORD_SUBMIT_FOR_APPROVAL: 'record.declaration-submit-for-approval',
RECORD_SUBMIT_FOR_UPDATES: 'record.declaration-submit-for-updates',
RECORD_DECLARATION_EDIT: 'record.declaration-edit',
RECORD_REVIEW_DUPLICATES: 'record.review-duplicates',
RECORD_DECLARATION_ARCHIVE: 'record.declaration-archive',
Expand All @@ -58,7 +58,7 @@ export const SCOPES = {
RECORD_EXPORT_RECORDS: 'record.export-records',
RECORD_DECLARATION_PRINT: 'record.declaration-print',
RECORD_PRINT_RECORDS_SUPPORTING_DOCUMENTS:
'record.declaration.print-supporting-documents',
'record.declaration-print-supporting-documents',
RECORD_REGISTRATION_PRINT: 'record.registration-print',
RECORD_PRINT_ISSUE_CERTIFIED_COPIES:
'record.registration-print&issue-certified-copies',
Expand Down Expand Up @@ -104,6 +104,7 @@ export const SCOPES = {
PERFORMANCE_EXPORT_VITAL_STATISTICS: 'performance.vital-statistics-export',

// organisation
ORGANISATION_READ: 'organisation.read',
ORGANISATION_READ_LOCATIONS: 'organisation.read-locations:all',
ORGANISATION_READ_LOCATIONS_MY_OFFICE:
'organisation.read-locations:my-office',
Expand All @@ -121,7 +122,10 @@ export const SCOPES = {
USER_UPDATE_MY_JURISDICTION: 'user.update:my-jurisdiction',

// config
CONFIG_UPDATE_ALL: 'config.update:all'
CONFIG_UPDATE_ALL: 'config.update:all',

// data seeding
USER_DATA_SEEDING: 'user.data-seeding'
} as const

export type Scope = (typeof SCOPES)[keyof typeof SCOPES]
Expand Down
2 changes: 1 addition & 1 deletion packages/config/src/config/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export default function getRoutes(): ServerRoute[] {
options: {
tags: ['api'],
auth: {
scope: [SCOPES.CONFIG_UPDATE_ALL]
scope: [SCOPES.CONFIG_UPDATE_ALL, SCOPES.USER_DATA_SEEDING]
},
description: 'Create a location',
validate: {
Expand Down
3 changes: 2 additions & 1 deletion packages/data-seeder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"tsconfig-paths": "^3.13.0",
"typescript": "4.9.5",
"uuid": "^3.3.2",
"zod": "^3.17.3"
"zod": "^3.17.3",
"zod-validation-error": "^1.3.1"
},
"lint-staged": {
"src/**/*.ts": [
Expand Down
8 changes: 4 additions & 4 deletions packages/data-seeder/src/locations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { OPENCRVS_SPECIFICATION_URL } from './constants'
import { env } from './environment'
import { TypeOf, z } from 'zod'
import { raise } from './utils'
import { inspect } from 'util'
import { fromZodError } from 'zod-validation-error'

const LocationSchema = z.array(
z.object({
Expand Down Expand Up @@ -161,9 +161,9 @@ async function getLocations() {
const parsedLocations = LocationSchema.safeParse(await res.json())
if (!parsedLocations.success) {
raise(
`Error when getting locations from country-config: ${inspect(
parsedLocations.error.issues
)}`
fromZodError(parsedLocations.error, {
prefix: `Error validating locations data returned from ${url}`
})
)
}
const adminStructureMap = validateAdminStructure(
Expand Down
54 changes: 37 additions & 17 deletions packages/data-seeder/src/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,31 @@ import { z } from 'zod'
import { parseGQLResponse, raise, delay } from './utils'
import { print } from 'graphql'
import gql from 'graphql-tag'
import { inspect } from 'util'
import { joinURL } from '@opencrvs/commons'
import { Scope, scopes } from '@opencrvs/commons/authentication'
import { fromZodError } from 'zod-validation-error'

const MAX_RETRY = 5
const RETRY_DELAY_IN_MILLISECONDS = 5000

type Roles = {
id: string
label: {
defaultMessage: string
description: string
id: string
}
scopes: string[]
}
const RoleSchema = z.array(
z.object({
id: z.string(),
label: z.object({
defaultMessage: z.string(),
description: z.string(),
id: z.string()
}),
scopes: z.array(
z.string().refine(
(scope) => scopes.includes(scope as Scope),
(invalidScope) => ({
message: `invalid scope "${invalidScope}" found\n`
})
)
)
})
)

const WithoutContact = z.object({
primaryOfficeId: z.string(),
Expand Down Expand Up @@ -84,27 +94,37 @@ async function getUsers(token: string) {

if (!parsedUsers.success) {
raise(
`Error when getting users metadata from country-config: ${inspect(
parsedUsers.error.issues
)}`
fromZodError(parsedUsers.error, {
prefix: `Error validating users metadata returned from ${url}`
})
)
}

const userRoles = parsedUsers.data.map((user) => user.role)

const rolesUrl = joinURL(env.COUNTRY_CONFIG_HOST, 'roles')

const response = await fetch(rolesUrl)
const rolesResponse = await fetch(rolesUrl)

if (!response.ok) raise(`Error fetching roles: ${response.status}`)
if (!rolesResponse.ok) raise(`Error fetching roles: ${rolesResponse.status}`)

const parsedRoles = RoleSchema.safeParse(await rolesResponse.json())

if (!parsedRoles.success) {
raise(
fromZodError(parsedRoles.error, {
prefix: `Validation failed for roles returned from ${rolesUrl}`
}).message
)
}

const allRoles: Roles[] = await response.json()
const allRoles = parsedRoles.data

let isConfigUpdateAllScopeAvailable = false
const configScope = 'config.update:all' as const

for (const userRole of userRoles) {
const currRole = allRoles.find((role: Roles) => role.id === userRole)
const currRole = allRoles.find((role) => role.id === userRole)
if (!currRole)
raise(`Role with id ${userRole} is not found in roles.json file`)
if (currRole.scopes.includes(configScope))
Expand Down
2 changes: 1 addition & 1 deletion packages/data-seeder/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"module": "commonjs",
"outDir": "build/dist",
"sourceMap": true,
"moduleResolution": "node",
"moduleResolution": "node16",
"rootDir": ".",
"lib": [
"esnext.asynciterable",
Expand Down
34 changes: 27 additions & 7 deletions packages/gateway/src/features/user/root-resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,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 { SCOPES } from '@opencrvs/commons/authentication'

export const resolvers: GQLResolver = {
Query: {
Expand Down Expand Up @@ -83,11 +84,16 @@ export const resolvers: GQLResolver = {
{ headers: authHeader }
) => {
// Only sysadmin or registrar or registration agent should be able to search user
if (!inScope(authHeader, ['sysadmin', 'register', 'validate'])) {
if (
!inScope(authHeader, [
SCOPES.USER_READ,
SCOPES.USER_READ_MY_JURISDICTION,
SCOPES.USER_READ_MY_OFFICE,
SCOPES.USER_DATA_SEEDING
])
) {
return await Promise.reject(
new Error(
'Search user is only allowed for sysadmin or registrar or registration agent'
)
new Error('Search user is not allowed for this user')
)
}

Expand Down Expand Up @@ -269,9 +275,15 @@ export const resolvers: GQLResolver = {
Mutation: {
async createOrUpdateUser(_, { user }, { headers: authHeader }) {
// Only sysadmin should be able to create user
if (!hasScope(authHeader, 'sysadmin')) {
if (
!inScope(authHeader, [
SCOPES.USER_DATA_SEEDING,
SCOPES.USER_CREATE,
SCOPES.USER_UPDATE
])
) {
return await Promise.reject(
new Error('Create user is only allowed for sysadmin')
new Error('Create or update user is not allowed for this user')
)
}

Expand Down Expand Up @@ -510,7 +522,15 @@ export const resolvers: GQLResolver = {
{ userId, action, reason, comment },
{ headers: authHeader }
) {
if (!hasScope(authHeader, 'sysadmin')) {
if (
!inScope(authHeader, [
SCOPES.USER_READ,
SCOPES.USER_READ_MY_JURISDICTION,
SCOPES.USER_READ_MY_OFFICE,
SCOPES.USER_UPDATE,
SCOPES.USER_DATA_SEEDING
])
) {
return await Promise.reject(
new Error(
`User ${userId} is not allowed to audit for not having the sys admin scope`
Expand Down
38 changes: 17 additions & 21 deletions packages/user-mgnt/src/config/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,14 +337,10 @@ export const getRoutes = () => {
options: {
auth: {
scope: [
SCOPES.RECORD_DECLARE_BIRTH,
SCOPES.RECORD_DECLARE_DEATH,
SCOPES.RECORD_DECLARE_MARRIAGE,
SCOPES.REGISTER,
SCOPES.CERTIFY,
SCOPES.PERFORMANCE_READ,
SCOPES.CONFIG_UPDATE_ALL,
SCOPES.RECORD_SUBMIT_FOR_APPROVAL
SCOPES.USER_READ,
SCOPES.USER_READ_MY_JURISDICTION,
SCOPES.USER_READ_MY_OFFICE,
SCOPES.USER_DATA_SEEDING
]
},
validate: {
Expand All @@ -362,16 +358,10 @@ export const getRoutes = () => {
description: 'Retrieves a user',
auth: {
scope: [
SCOPES.RECORD_DECLARE_BIRTH,
SCOPES.RECORD_DECLARE_DEATH,
SCOPES.RECORD_DECLARE_MARRIAGE,
SCOPES.REGISTER,
SCOPES.CERTIFY,
SCOPES.PERFORMANCE_READ,
SCOPES.CONFIG_UPDATE_ALL,
SCOPES.RECORD_SUBMIT_FOR_APPROVAL,
SCOPES.RECORD_REGISTRATION_VERIFY_CERTIFIED_COPIES,
SCOPES.RECORDSEARCH
SCOPES.USER_READ,
SCOPES.USER_READ_MY_JURISDICTION,
SCOPES.USER_READ_MY_OFFICE,
SCOPES.USER_DATA_SEEDING
]
},
validate: {
Expand All @@ -387,7 +377,7 @@ export const getRoutes = () => {
tags: ['api'],
description: 'Creates a new user',
auth: {
scope: [SCOPES.CONFIG_UPDATE_ALL]
scope: [SCOPES.CONFIG_UPDATE_ALL, SCOPES.USER_DATA_SEEDING]
}
}
},
Expand All @@ -399,7 +389,7 @@ export const getRoutes = () => {
tags: ['api'],
description: 'Updates an existing user',
auth: {
scope: [SCOPES.CONFIG_UPDATE_ALL]
scope: [SCOPES.CONFIG_UPDATE_ALL, SCOPES.USER_DATA_SEEDING]
}
}
},
Expand Down Expand Up @@ -452,7 +442,13 @@ export const getRoutes = () => {
handler: userAuditHandler,
options: {
auth: {
scope: [SCOPES.CONFIG_UPDATE_ALL]
scope: [
SCOPES.USER_UPDATE,
SCOPES.USER_READ,
SCOPES.USER_READ_MY_JURISDICTION,
SCOPES.USER_READ_MY_OFFICE,
SCOPES.USER_DATA_SEEDING
]
},
validate: {
payload: userAuditSchema
Expand Down

0 comments on commit fc1b3d2

Please sign in to comment.