Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ API_DOC_URL = "/entity-management/api-doc"
#Indicate If auth token is bearer or not
IS_AUTH_TOKEN_BEARER=false

AUTH_METHOD = native #or keycloak_public_key
KEYCLOAK_PUBLIC_KEY_PATH = path to the pem/secret file
Original file line number Diff line number Diff line change
Expand Up @@ -831,7 +831,7 @@
}
],
"url": {
"raw": "{{baseUrl}}/entity/v1/entities/targetedRoles/5f33c3d85f637784791cd831",
"raw": "{{baseUrl}}/entity/v1/entities/targetedRoles/5f33c3d85f637784791cd831?entityType=district",
"host": ["{{baseUrl}}"],
"path": ["entity", "v1", "entities", "targetedRoles", "5f33c3d85f637784791cd831"]
}
Expand Down
8 changes: 7 additions & 1 deletion src/api-doc/api-doc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1767,7 +1767,7 @@ paths:
param: _id
msg: required Entity id
/v1/entities/listByLocationIds:
POST:
post:
summary: List the entities based on location id
tags:
- entities-API's
Expand Down Expand Up @@ -2294,6 +2294,12 @@ paths:
schema:
type: string
required: true
- in: query
name: entityType
description: Please append a valid entity type to retrieve the roles associated with its higher-level entity types.
schema:
type: string
required: true
responses:
'200':
description: Accepted
Expand Down
3 changes: 2 additions & 1 deletion src/controllers/v1/entities.js
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,8 @@ module.exports = class Entities extends Abstract {
req.params._id,
req.pageNo,
req.pageSize,
req?.query?.paginate?.toLowerCase() == 'true' ? true : false
req?.query?.paginate?.toLowerCase() == 'true' ? true : false,
req.query.entityType ? req.query.entityType : ''
)
// Resolves the promise with the retrieved entity data
return resolve(userRoleDetails)
Expand Down
10 changes: 10 additions & 0 deletions src/envVariables.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ let enviromentVariables = {
optional: true,
default: false,
},
AUTH_METHOD: {
message: 'Required authentication method',
optional: true,
default: CONSTANTS.common.AUTH_METHOD.NATIVE,
},
KEYCLOAK_PUBLIC_KEY_PATH: {
message: 'Required Keycloak Public Key Path',
optional: true,
default: '../keycloakPublicKeys',
},
}

let success = true
Expand Down
4 changes: 4 additions & 0 deletions src/generics/constants/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,8 @@ module.exports = {
GET_METHOD: 'GET',
ENTITYTYPE: 'entityType',
GROUPS: 'groups',
AUTH_METHOD: {
NATIVE: 'native',
KEYCLOAK_PUBLIC_KEY: 'keycloak_public_key',
},
}
2 changes: 2 additions & 0 deletions src/generics/keycloakPublicKeys/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*
!.gitignore
87 changes: 79 additions & 8 deletions src/generics/middleware/authenticator.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
// dependencies
const jwt = require('jsonwebtoken')
const isBearerRequired = process.env.IS_AUTH_TOKEN_BEARER === 'true'
const path = require('path')
const fs = require('fs')
var respUtil = function (resp) {
return {
status: resp.errCode,
Expand Down Expand Up @@ -86,29 +88,98 @@ module.exports = async function (req, res, next, token = '') {
token = token?.trim()
}

rspObj.errCode = CONSTANTS.apiResponses.TOKEN_INVALID_CODE
rspObj.errMsg = CONSTANTS.apiResponses.TOKEN_INVALID_MESSAGE
rspObj.responseCode = HTTP_STATUS_CODE['unauthorized'].status

// <---- For Elevate user service user compactibility ---->
let decodedToken = null
try {
decodedToken = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET)
if (process.env.AUTH_METHOD === CONSTANTS.common.AUTH_METHOD.NATIVE) {
try {
// If using native authentication, verify the JWT using the secret key
decodedToken = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET)
} catch (err) {
// If verification fails, send an unauthorized response
rspObj.errCode = CONSTANTS.apiResponses.TOKEN_MISSING_CODE
rspObj.errMsg = CONSTANTS.apiResponses.TOKEN_MISSING_MESSAGE
rspObj.responseCode = HTTP_STATUS_CODE['unauthorized'].status
return res.status(HTTP_STATUS_CODE['unauthorized'].status).send(respUtil(rspObj))
}
} else if (process.env.AUTH_METHOD === CONSTANTS.common.AUTH_METHOD.KEYCLOAK_PUBLIC_KEY) {
// If using Keycloak with a public key for authentication
const keycloakPublicKeyPath = `${process.env.KEYCLOAK_PUBLIC_KEY_PATH}/`
const PEM_FILE_BEGIN_STRING = '-----BEGIN PUBLIC KEY-----'
const PEM_FILE_END_STRING = '-----END PUBLIC KEY-----'

// Decode the JWT to extract its claims without verifying
const tokenClaims = jwt.decode(token, { complete: true })

if (!tokenClaims || !tokenClaims.header) {
// If the token does not contain valid claims or header, send an unauthorized response
rspObj.errCode = CONSTANTS.apiResponses.TOKEN_MISSING_CODE
rspObj.errMsg = CONSTANTS.apiResponses.TOKEN_MISSING_MESSAGE
rspObj.responseCode = HTTP_STATUS_CODE['unauthorized'].status
return res.status(HTTP_STATUS_CODE['unauthorized'].status).send(respUtil(rspObj))
}

// Extract the key ID (kid) from the token header
const kid = tokenClaims.header.kid

// Construct the path to the public key file using the key ID
let filePath = path.resolve(__dirname, keycloakPublicKeyPath, kid.replace(/\.\.\//g, ''))

// Read the public key file from the resolved file path
const accessKeyFile = await fs.promises.readFile(filePath, 'utf8')

// Ensure the public key is properly formatted with BEGIN and END markers
const cert = accessKeyFile.includes(PEM_FILE_BEGIN_STRING)
? accessKeyFile
: `${PEM_FILE_BEGIN_STRING}\n${accessKeyFile}\n${PEM_FILE_END_STRING}`
let verifiedClaims
try {
// Verify the JWT using the public key and specified algorithms
verifiedClaims = jwt.verify(token, cert, { algorithms: ['sha1', 'RS256', 'HS256'] })
} catch (err) {
// If the token is expired or any other error occurs during verification
if (err.name === 'TokenExpiredError') {
rspObj.errCode = CONSTANTS.apiResponses.TOKEN_INVALID_CODE
rspObj.errMsg = CONSTANTS.apiResponses.TOKEN_INVALID_MESSAGE
rspObj.responseCode = HTTP_STATUS_CODE['unauthorized'].status
return res.status(HTTP_STATUS_CODE['unauthorized'].status).send(respUtil(rspObj))
}
}

// Extract the external user ID from the verified claims
const externalUserId = verifiedClaims.sub.split(':').pop()

const data = {
id: externalUserId,
roles: [], // this is temporariy set to an empty array, it will be corrected soon...
name: verifiedClaims.name,
organization_id: verifiedClaims.org || null,
}

// Ensure decodedToken is initialized as an object
decodedToken = decodedToken || {}
decodedToken['data'] = data
}
} catch (err) {
rspObj.errCode = CONSTANTS.apiResponses.TOKEN_MISSING_CODE
rspObj.errMsg = CONSTANTS.apiResponses.TOKEN_MISSING_MESSAGE
rspObj.responseCode = HTTP_STATUS_CODE['unauthorized'].status
return res.status(HTTP_STATUS_CODE['unauthorized'].status).send(respUtil(rspObj))
}
if (!decodedToken) {
rspObj.errCode = CONSTANTS.apiResponses.TOKEN_MISSING_CODE
rspObj.errMsg = CONSTANTS.apiResponses.TOKEN_MISSING_MESSAGE
rspObj.responseCode = HTTP_STATUS_CODE['unauthorized'].status
return res.status(HTTP_STATUS_CODE['unauthorized'].status).send(respUtil(rspObj))
}

req.userDetails = {
userToken: token,
userInformation: {
userId: decodedToken.data.id.toString(),
userId: typeof decodedToken.data.id == 'string' ? decodedToken.data.id : decodedToken.data.id.toString(),
userName: decodedToken.data.name,
// email : decodedToken.data.email, //email is removed from token
firstName: decodedToken.data.name,
roles: decodedToken.data.roles.map((role) => role.title),
entityTypes: 'state',
},
}
next()
Expand Down
18 changes: 15 additions & 3 deletions src/module/entities/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,10 @@ module.exports = class UserProjectsHelper {
* @param {Array<string>} entityId - An array of entity IDs to filter roles.
* @param {params} pageSize - page pageSize.
* @param {params} pageNo - page no.
* @param {String} type - Entity type
* @returns {Promise<Object>} A promise that resolves to the response containing the fetched roles or an error object.
*/
static targetedRoles(entityId, pageNo = '', pageSize = '', paginate) {
static targetedRoles(entityId, pageNo = '', pageSize = '', paginate, type = "") {
return new Promise(async (resolve, reject) => {
try {
// Construct the filter to retrieve entities based on provided entity IDs
Expand All @@ -227,6 +228,7 @@ module.exports = class UserProjectsHelper {
const projectionFields = ['childHierarchyPath', 'entityType']
// Retrieve entityDetails based on provided entity IDs
const entityDetails = await entitiesQueries.entityDocuments(filter, projectionFields)

if (
!entityDetails ||
!entityDetails[0]?.childHierarchyPath ||
Expand All @@ -241,12 +243,22 @@ module.exports = class UserProjectsHelper {
const { childHierarchyPath, entityType } = entityDetails[0]

// Append entityType to childHierarchyPath array
const updatedChildHierarchyPaths = [...childHierarchyPath, entityType]
const updatedChildHierarchyPaths = [entityType, ...childHierarchyPath]

// Filter for higher entity types if a specific type is requested
let filteredHierarchyPaths = updatedChildHierarchyPaths
if (type) {
const typeIndex = updatedChildHierarchyPaths.indexOf(type)
if (typeIndex > -1) {
// Include only higher types in the hierarchy
filteredHierarchyPaths = updatedChildHierarchyPaths.slice(0, typeIndex + 1)
}
}

// Construct the filter to retrieve entity type IDs based on child hierarchy paths
const entityTypeFilter = {
name: {
$in: updatedChildHierarchyPaths,
$in: filteredHierarchyPaths,
},
isDeleted: false,
}
Expand Down
7 changes: 6 additions & 1 deletion src/module/entities/validator/v1.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ module.exports = (req) => {
let entitiesValidator = {
add: function () {
req.checkQuery('type').exists().withMessage('required type')
req.checkBody('externalId').exists().withMessage('required externalId ')
req.checkBody('externalId')
.exists()
.withMessage('The externalId field is required.')
.trim() // Removes leading and trailing spaces
.notEmpty()
.withMessage('The externalId field cannot be empty.')
req.checkBody('name')
.exists()
.withMessage('The name field is required.')
Expand Down
7 changes: 6 additions & 1 deletion src/module/entityTypes/validator/v1.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ module.exports = (req, res) => {
update: function () {
req.checkParams('_id').exists().withMessage('required _id')
req.checkParams('_id').exists().isMongoId().withMessage('Invalid EntityType ID')
req.checkBody('name').exists().withMessage('required name')
req.checkBody('name')
.exists()
.withMessage('The name field is required.')
.trim()
.notEmpty()
.withMessage('The name field cannot be empty.')
},
create: function () {
req.checkBody('name')
Expand Down
17 changes: 17 additions & 0 deletions src/module/userRoleExtension/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,23 @@ module.exports = class userRoleExtensionHelper {
static update(userRoleId, bodyData) {
return new Promise(async (resolve, reject) => {
try {
await Promise.all(
bodyData.entityTypes.map(async (entityTypeData) => {
// Validate that both entityType and entityTypeId exist in the entityType DB
let existingEntityType = await entityTypeQueries.findOne({
name: entityTypeData.entityType,
_id: ObjectId(entityTypeData.entityTypeId),
})

if (!existingEntityType) {
// If any entityType is invalid, reject the request
throw {
status: HTTP_STATUS_CODE.bad_request.status,
message: `EntityType '${entityTypeData.entityType}' with ID '${entityTypeData.entityTypeId}' does not exist.`,
}
}
})
)
// Find and update the user role extension based on the provided userRoleId and bodyData
let userInformation = await userRoleExtensionQueries.findOneAndUpdate(
{ _id: ObjectId(userRoleId) },
Expand Down