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
25 changes: 6 additions & 19 deletions src/controllers/v1/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,25 +73,12 @@ module.exports = class Admin {
deleteEntity(req) {
return new Promise(async (resolve, reject) => {
try {
let deletedEntity
if (
req.userDetails &&
req.userDetails.userInformation &&
req.userDetails.userInformation.roles &&
req.userDetails.userInformation.roles.includes(CONSTANTS.common.ADMIN_ROLE)
) {
deletedEntity = await adminHelper.allowRecursiveDelete(
req.params._id,
req.query.allowRecursiveDelete == 'true' ? 'true' : 'false',
req.userDetails.tenantAndOrgInfo.tenantId,
req.userDetails.userInformation.userId
)
} else {
throw {
status: HTTP_STATUS_CODE.forbidden.status,
message: CONSTANTS.apiResponses.ADMIN_ROLE_REQUIRED,
}
}
let deletedEntity = await adminHelper.allowRecursiveDelete(
req.params._id,
req.query.allowRecursiveDelete == 'true' ? 'true' : 'false',
req.userDetails.tenantAndOrgInfo.tenantId,
req.userDetails.userInformation.userId
)
return resolve(deletedEntity)
} catch (error) {
return reject({
Expand Down
5 changes: 3 additions & 2 deletions src/databaseQueries/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ module.exports = class Admin {
*
* @param {String} entityType - Type of the entity (e.g., 'block', 'cluster').
* @param {ObjectId} entityId - MongoDB ObjectId of the entity to remove from groups.
* @param {String} tenantId - Tenant ID to scope the operation.
* @returns {Promise<Object>} - MongoDB updateMany result containing modified count.
*/
static pullEntityFromGroups(entityType, entityId) {
static pullEntityFromGroups(entityType, entityId, tenantId) {
return new Promise(async (resolve, reject) => {
try {
//Build the $pull query to remove the entityId from group arrays
Expand All @@ -49,7 +50,7 @@ module.exports = class Admin {
},
}
const result = await database.models.entities.updateMany(
{ [`groups.${entityType}`]: entityId },
{ [`groups.${entityType}`]: entityId, tenantId: tenantId },
updateQuery
)
return resolve(result)
Expand Down
4 changes: 2 additions & 2 deletions src/databaseQueries/deletionAuditLogs.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ module.exports = class deletionAuditLogs {
* Inserts deletion audit logs into the deletionAuditLogs collection.
*
* @method
* @name deletionAuditLogs
* @name create
* @param {Array<Object>} logs - Array of log objects containing:
* - entityId: ObjectId (ID of the deleted entity)
* - deletedBy: String | Number (User who deleted the entity)
* - deletedAt: String (ISO date string of when deletion occurred)
* @returns {Promise<Array<Object>>} - Inserted log documents on success.
* @throws {Object} - On failure, returns an error object with status and message.
*/
static deletionAuditLogs(logs) {
static create(logs) {
return new Promise(async (resolve, reject) => {
try {
let deletedData = await database.models.deletionAuditLogs.insertMany(logs)
Expand Down
181 changes: 181 additions & 0 deletions src/generics/middleware/checkAdminRole.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/**
* name : checkAdminRole.js
* author : Mallanagouda R Biradar
* Date : 7-Aug-2025
* Description : checkAdminRole middleware.
*/

const jwt = require('jsonwebtoken')
const path = require('path')
const fs = require('fs')
const _ = require('lodash')
var respUtil = function (resp) {
return {
status: resp.errCode,
message: resp.errMsg,
currentDate: new Date().toISOString(),
}
}

module.exports = async function (req, res, next) {
// Define paths that require admin role validation
let adminPath = ['admin/deleteEntity']
// Initialize response object for error formatting
let rspObj = {}
// Flag to check if the current request path needs admin validation
let requiresAdminValidation = false
// Check if the incoming request path matches any admin paths
await Promise.all(
adminPath.map(async function (path) {
if (req.path.includes(path)) {
requiresAdminValidation = true
}
})
)

// If path needs admin check, validate the user's role using JWT token
if (requiresAdminValidation) {
// Get token from request headers
const token = req.headers['x-auth-token']

// If no token found, return unauthorized error
if (!token) {
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))
}

let decodedToken
try {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MallanagoudaB we should also take the scenario of keycloak usage as well

// Decode and verify JWT token using secret key
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 (error) {
return res.status(HTTP_STATUS_CODE.unauthorized.status).send(
respUtil({
errCode: CONSTANTS.apiResponses.TOKEN_MISSING_CODE,
errMsg: CONSTANTS.apiResponses.TOKEN_MISSING_MESSAGE,
responseCode: HTTP_STATUS_CODE['unauthorized'].status,
})
)
}

// Path to config.json
const configFilePath = path.resolve(__dirname, '../../config.json')
// Initialize variables
let configData = {}
let defaultRoleExtraction
// Check if config.json exists
if (fs.existsSync(configFilePath)) {
// Read and parse the config.json file
const rawData = fs.readFileSync(configFilePath)
try {
configData = JSON.parse(rawData)
if (!configData.userRolesInformationKey) {
defaultRoleExtraction = decodedToken.data.organizations[0].roles
} else {
defaultRoleExtraction = _.get({ decodedToken }, configData.userRolesInformationKey)
}
} catch (error) {
console.error('Error parsing config.json:', error)
}
} else {
defaultRoleExtraction = decodedToken.data.organizations[0].roles
}

if (!defaultRoleExtraction) {
rspObj.errCode = CONSTANTS.apiResponses.TENANTID_AND_ORGID_REQUIRED_IN_TOKEN_CODE
rspObj.errMsg = CONSTANTS.apiResponses.TENANTID_AND_ORGID_REQUIRED_IN_TOKEN_MESSAGE
rspObj.responseCode = HTTP_STATUS_CODE['unauthorized'].status
return res.status(HTTP_STATUS_CODE['unauthorized'].status).send(respUtil(rspObj))
}

// Convert roles array to list of role titles
let roles = defaultRoleExtraction.map((roles) => {
return roles.title
})

// Check if user has the admin role
if (roles.includes(CONSTANTS.common.ADMIN_ROLE)) {
// If admin, allow the request to continue
return next()
} else {
// If not admin, throw forbidden error
return next({
status: responseCode.forbidden.status,
message: reqMsg.ADMIN_ROLE_REQUIRED,
})
}
}

next()
return
}
21 changes: 13 additions & 8 deletions src/module/admin/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ module.exports = class AdminHelper {
try {
//Fetch the entity document to validate existence and get its metadata
const filter = { _id: entityId, tenantId: tenantId }

const entityDoc = await entitiesQueries.entityDocuments(filter, ['groups', 'entityType'])

if (!entityDoc.length) {
Expand Down Expand Up @@ -113,6 +114,7 @@ module.exports = class AdminHelper {
// Delete all entities collected (root + nested groups)
const deletedEntities = await entitiesQueries.removeDocuments({
_id: { $in: relatedEntityObjectIds },
tenantId: tenantId,
})

// Perform post-deletion tasks: unlinking, logging, and pushing Kafka events
Expand All @@ -134,7 +136,10 @@ module.exports = class AdminHelper {
})
} else {
// If recursive deletion is not allowed, delete only the single entity
const deletedEntities = await entitiesQueries.removeDocuments({ _id: entityObjectId })
const deletedEntities = await entitiesQueries.removeDocuments({
_id: entityObjectId,
tenantId: tenantId,
})

// Perform post-deletion tasks: unlinking, logging, and pushing Kafka event
const { unLinkedEntitiesCount } = await this.handlePostEntityDeletionTasks(
Expand Down Expand Up @@ -180,18 +185,18 @@ module.exports = class AdminHelper {
static handlePostEntityDeletionTasks(deletedIds, entityType, deletedBy, tenantId) {
return new Promise(async (resolve, reject) => {
// Remove from other entities' groups
const unlinkResult = await adminQueries.pullEntityFromGroups(entityType, deletedIds[0])
const unlinkResult = await adminQueries.pullEntityFromGroups(entityType, deletedIds[0], tenantId)

// Insert logs into deletionAuditLogs collection
await this.logDeletion(deletedIds, deletedBy)
await this.create(deletedIds, deletedBy)

// Message: {"topic":"RESOURCE_DELETION_TOPIC","value":"{\"entity\":\"resource\",\"type\":\"entity\",\"eventType\":\"delete\
// ",\"entityIds\":[\"6852c9027248c20014b38b69\",\"6852c9227248c20014b3957d\",\"6852c9227248c20014b3957e\",\"6852c9227248c20014b3957f\
// ",\"6852c9227248c20014b39580\",\"6852c9227248c20014b39581\",\"6852c9227248c20014b39582\",\"6852c9227248c20014b39583\",
// \"deleted_By\":1,\"tenant_code\":\"shikshalokam\"}"
// Push Kafka events
await this.pushEntityDeleteKafkaEvent(deletedIds, deletedBy, tenantId)
resolve({
return resolve({
unLinkedEntitiesCount: unlinkResult?.nModified || 0,
})
})
Expand All @@ -201,13 +206,13 @@ module.exports = class AdminHelper {
* Logs deletion entries for one or more entities into the `deletionAuditLogs` collection.
*
* @method
* @name logDeletion
* @name create
* @param {Array<String|ObjectId>} entityIds - Array of entity IDs (as strings or ObjectIds) to log deletion for.
* @param {String|Number} deletedBy - User ID (or 'SYSTEM') who performed the deletion.
*
* @returns {Promise<Object>} - Returns success status or error information.
*/
static logDeletion(entityIds, deletedBy) {
static create(entityIds, deletedBy) {
return new Promise(async (resolve, reject) => {
try {
// Prepare log entries
Expand All @@ -216,8 +221,8 @@ module.exports = class AdminHelper {
deletedBy: deletedBy || 'SYSTEM',
deletedAt: new Date().toISOString(),
}))
// Insert logs into deletionAuditLogs collection
await deletionAuditQueries.deletionAuditLogs(logs)
// Insert logs into create collection
await deletionAuditQueries.create(logs)
return resolve({ success: true })
} catch (error) {
resolve({
Expand Down
Loading