Skip to content

Commit

Permalink
Merge pull request #7301 from opencrvs/configurable-roles
Browse files Browse the repository at this point in the history
feat!: country configurable user scopes & roles
  • Loading branch information
Zangetsu101 authored Jan 23, 2025
2 parents 8b4cf5b + 0f96472 commit a942393
Show file tree
Hide file tree
Showing 389 changed files with 11,387 additions and 15,039 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/build-images-from-branch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ jobs:
# another workflow is calling this one
if [ "${{ github.event_name }}" == 'push' ]; then
BRANCH=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}
BRANCH=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}
elif [ "${{ github.event_name }}" == 'pull_request' ]; then
BRANCH=${{ github.event.pull_request.head.ref }}
BRANCH=${{ github.event.pull_request.head.ref }}
else
BRANCH=${{ inputs.branch_name }}
fi
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
### Breaking changes

- **Dashboard:** Changes made to the dashboard configuration will reset after upgrading OpenCRVS.
- Removed unused searchBirthRegistrations and searchDeathRegistrations queries, as they are no longer used by the client.
- **Retrieve action deprecated:** Field agents & registration agents used to be able to retrieve records to view the audit history & PII. We are removing this in favor of audit capabilities that is planned for in a future release.

### New features

Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ services:
- CONFIG_SMS_CODE_EXPIRY_SECONDS=600
- NOTIFICATION_SERVICE_URL=http://notification:2020/
- METRICS_URL=http://metrics:1050
- COUNTRY_CONFIG_URL_INTERNAL=http://countryconfig:3040
user-mgnt:
image: opencrvs/ocrvs-user-mgnt:${VERSION}
#platform: linux/amd64
Expand Down
5 changes: 3 additions & 2 deletions packages/auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"scripts": {
"start": "cross-env NODE_ENV=development NODE_OPTIONS=--dns-result-order=ipv4first nodemon --exec ts-node -r tsconfig-paths/register src/index.ts",
"start:prod": "TS_NODE_BASEURL=./build/dist/src node -r tsconfig-paths/register build/dist/src/index.js",
"test": "jest --coverage --silent --noStackTrace && yarn test:compilation",
"test": "yarn test:compilation && jest --coverage --silent --noStackTrace",
"test:watch": "jest --watch",
"open:cov": "yarn test && opener coverage/index.html",
"lint": "eslint -c .eslintrc.js --fix ./src --max-warnings=0",
Expand Down Expand Up @@ -83,7 +83,8 @@
"<rootDir>"
],
"moduleNameMapper": {
"@auth/(.*)": "<rootDir>/src/$1"
"@auth/(.*)": "<rootDir>/src/$1",
"@opencrvs/commons/(.*)": "<rootDir>/../commons/src/$1"
},
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$",
"setupFiles": [
Expand Down
4 changes: 2 additions & 2 deletions packages/auth/resources/generate-test-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
*
* Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS.
*/
import * as commandLineArgs from 'command-line-args'
import * as commandLineUsage from 'command-line-usage'
import commandLineArgs from 'command-line-args'
import commandLineUsage from 'command-line-usage'
import { join } from 'path'

const optionList = [
Expand Down
4 changes: 2 additions & 2 deletions packages/auth/resources/request-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
*
* Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS.
*/
import * as commandLineArgs from 'command-line-args'
import * as commandLineUsage from 'command-line-usage'
import commandLineArgs from 'command-line-args'
import commandLineUsage from 'command-line-usage'
import fetch from 'node-fetch'
import * as readline from 'readline'

Expand Down
3 changes: 2 additions & 1 deletion packages/auth/src/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ export const env = cleanEnv(process.env, {
METRICS_URL: url({ devDefault: 'http://localhost:1050' }),
NOTIFICATION_SERVICE_URL: url({ devDefault: 'http://localhost:2020/' }),
DOMAIN: str({ devDefault: '*' }),
COUNTRY_CONFIG_URL: url({ devDefault: 'http://localhost:3040/' }),
COUNTRY_CONFIG_URL: url({ devDefault: 'http://localhost:3040/' }), // used for external requests (CORS whitelist)
COUNTRY_CONFIG_URL_INTERNAL: url({ devDefault: 'http://localhost:3040/' }), // used for internal service-to-service communication
LOGIN_URL: url({ devDefault: 'http://localhost:3020/' }),
CLIENT_APP_URL: url({ devDefault: 'http://localhost:3000/' }),
CERT_PRIVATE_KEY_PATH: str({ devDefault: '../../.secrets/private-key.pem' }),
Expand Down
26 changes: 18 additions & 8 deletions packages/auth/src/features/authenticate/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
*/
import * as fetchAny from 'jest-fetch-mock'
import { createProductionEnvironmentServer } from '@auth/tests/util'
import { AuthServer, createServer } from '@auth/server'
import { createServer, AuthServer } from '@auth/server'
import { DEFAULT_ROLES_DEFINITION } from '@opencrvs/commons/authentication'

const fetch = fetchAny as fetchAny.FetchMock
describe('authenticate handler receives a request', () => {
Expand Down Expand Up @@ -54,7 +55,7 @@ describe('authenticate handler receives a request', () => {
it('returns 403', async () => {
fetch.mockResponse(
JSON.stringify({
userId: '1',
id: '1',
status: 'deactivated',
scope: ['admin']
})
Expand All @@ -78,14 +79,18 @@ describe('authenticate handler receives a request', () => {

jest.spyOn(reloadedCodeService, 'generateNonce').mockReturnValue('12345')

fetch.mockResponse(
fetch.mockResponseOnce(
JSON.stringify({
userId: '1',
id: '1',
status: 'active',
scope: ['admin'],
role: 'NATIONAL_SYSTEM_ADMIN',
mobile: `+345345343`
})
)

fetch.mockResponse(JSON.stringify(DEFAULT_ROLES_DEFINITION), {
status: 200
})
const spy = jest.spyOn(reloadedCodeService, 'sendVerificationCode')

await server.server.inject({
Expand All @@ -109,14 +114,19 @@ describe('authenticate handler receives a request', () => {

jest.spyOn(reloadedCodeService, 'generateNonce').mockReturnValue('12345')

fetch.mockResponse(
fetch.mockResponseOnce(
JSON.stringify({
userId: '1',
id: '1',
status: 'pending',
scope: ['admin'],
role: 'NATIONAL_SYSTEM_ADMIN',
mobile: `+345345343`
})
)

fetch.mockResponse(JSON.stringify(DEFAULT_ROLES_DEFINITION), {
status: 200
})

const spy = jest.spyOn(reloadedCodeService, 'sendVerificationCode')

await server.server.inject({
Expand Down
27 changes: 18 additions & 9 deletions packages/auth/src/features/authenticate/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,22 @@
*
* Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS.
*/
import * as Hapi from '@hapi/hapi'
import * as Joi from 'joi'
import { JWT_ISSUER, WEB_USER_JWT_AUDIENCES } from '@auth/constants'
import {
IAuthentication,
authenticate,
storeUserInformation,
createToken,
generateAndSendVerificationCode,
IAuthentication
storeUserInformation
} from '@auth/features/authenticate/service'
import {
NotificationEvent,
generateNonce
} from '@auth/features/verifyCode/service'
import { unauthorized, forbidden } from '@hapi/boom'
import { WEB_USER_JWT_AUDIENCES, JWT_ISSUER } from '@auth/constants'
import { forbidden, unauthorized } from '@hapi/boom'
import * as Hapi from '@hapi/hapi'
import * as Joi from 'joi'
import { getUserRoleScopeMapping } from '@auth/features/scopes/service'

interface IAuthPayload {
username: string
Expand All @@ -43,6 +44,7 @@ export default async function authenticateHandler(
): Promise<IAuthResponse> {
const payload = request.payload as IAuthPayload
let result: IAuthentication

const { username, password } = payload
try {
result = await authenticate(username.trim(), password)
Expand All @@ -63,10 +65,15 @@ export default async function authenticateHandler(

const isPendingUser = response.status && response.status === 'pending'

const roleScopeMappings = await getUserRoleScopeMapping()

const role = result.role as keyof typeof roleScopeMappings
const scopes = roleScopeMappings[role]

if (isPendingUser) {
response.token = await createToken(
result.userId,
result.scope,
scopes,
WEB_USER_JWT_AUDIENCES,
JWT_ISSUER
)
Expand All @@ -75,7 +82,7 @@ export default async function authenticateHandler(
nonce,
result.name,
result.userId,
result.scope,
scopes,
result.mobile,
result.email
)
Expand All @@ -84,13 +91,14 @@ export default async function authenticateHandler(

await generateAndSendVerificationCode(
nonce,
result.scope,
scopes,
notificationEvent,
result.name,
result.mobile,
result.email
)
}

return response
}

Expand All @@ -104,6 +112,7 @@ export const responseSchema = Joi.object({
mobile: Joi.string().optional(),
email: Joi.string().optional(),
status: Joi.string(),
role: Joi.string(),
token: Joi.string().optional()
})

Expand Down
24 changes: 10 additions & 14 deletions packages/auth/src/features/authenticate/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ import {
} from '@auth/features/verifyCode/service'
import { logger, UUID } from '@opencrvs/commons'
import { unauthorized } from '@hapi/boom'
import { chainW, tryCatch } from 'fp-ts/Either'
import { pipe } from 'fp-ts/function'
import * as F from 'fp-ts'
import { Scope } from '@opencrvs/commons/authentication'
const { chainW, tryCatch } = F.either
const { pipe } = F.function
import { env } from '@auth/environment'

const cert = readFileSync(env.CERT_PRIVATE_KEY_PATH)
Expand All @@ -48,14 +50,14 @@ export interface IAuthentication {
mobile?: string
userId: string
status: string
scope: string[]
email?: string
role: string
}

export interface ISystemAuthentication {
systemId: string
status: string
scope: string[]
scope: Scope[]
}

export class UserInfoNotFoundError extends Error {}
Expand All @@ -79,11 +81,13 @@ export async function authenticate(
if (res.status !== 200) {
throw Error(res.statusText)
}

const body = await res.json()

return {
name: body.name,
userId: body.id,
scope: body.scope,
role: body.role,
status: body.status,
mobile: body.mobile,
email: body.email
Expand Down Expand Up @@ -121,9 +125,6 @@ export async function createToken(
issuer: string,
temporary?: boolean
): Promise<string> {
if (typeof userId === undefined) {
throw new Error('Invalid userId found for token creation')
}
return sign({ scope }, cert, {
subject: userId,
algorithm: 'RS256',
Expand Down Expand Up @@ -201,12 +202,7 @@ export async function generateAndSendVerificationCode(
email?: string
) {
const isDemoUser = scope.indexOf('demo') > -1 || env.QA_ENV
logger.info(
`isDemoUser,
${JSON.stringify({
isDemoUser: isDemoUser
})}`
)
logger.info(`Is demo user: ${isDemoUser}. Scopes: ${scope.join(', ')}`)
let verificationCode
if (isDemoUser) {
verificationCode = '000000'
Expand Down
14 changes: 13 additions & 1 deletion packages/auth/src/features/authenticateSuperUser/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
} from '@auth/features/authenticate/service'
import { unauthorized } from '@hapi/boom'
import { WEB_USER_JWT_AUDIENCES, JWT_ISSUER } from '@auth/constants'
import { Scope, SCOPES } from '@opencrvs/commons/authentication'
import { logger } from '@opencrvs/commons'

interface IAuthPayload {
username: string
Expand All @@ -36,9 +38,19 @@ export default async function authenticateSuperUserHandler(
throw unauthorized()
}

if (result.status === 'deactivated') {
logger.info('Login attempt with a deactivated super user account detected')
throw unauthorized()
}

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

const token = await createToken(
result.userId,
result.scope,
SUPER_ADMIN_SCOPES,
WEB_USER_JWT_AUDIENCES,
JWT_ISSUER
)
Expand Down
3 changes: 2 additions & 1 deletion packages/auth/src/features/oauthToken/client-credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
NOTIFICATION_API_USER_AUDIENCE
} from '@auth/constants'
import * as oauthResponse from './responses'
import { SCOPES } from '@opencrvs/commons/authentication'

export async function clientCredentialsHandler(
request: Hapi.Request,
Expand All @@ -43,7 +44,7 @@ export async function clientCredentialsHandler(
return oauthResponse.invalidClient(h)
}

const isNotificationAPIUser = result.scope.includes('notification-api')
const isNotificationAPIUser = result.scope.includes(SCOPES.NOTIFICATION_API)

const token = await createToken(
result.systemId,
Expand Down
6 changes: 4 additions & 2 deletions packages/auth/src/features/oauthToken/token-exchange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ import {
import { pipe } from 'fp-ts/lib/function'
import { UUID } from '@opencrvs/commons'

const SUBJECT_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token'
const RECORD_TOKEN_TYPE = 'urn:opencrvs:oauth:token-type:single_record_token'
export const SUBJECT_TOKEN_TYPE =
'urn:ietf:params:oauth:token-type:access_token'
export const RECORD_TOKEN_TYPE =
'urn:opencrvs:oauth:token-type:single_record_token'

/**
* Allows creating record-specific tokens for when a 3rd party system needs to confirm a registration
Expand Down
Loading

0 comments on commit a942393

Please sign in to comment.