From c70e65e58c28b1e32d74fdd0123a8d1fad8140ca Mon Sep 17 00:00:00 2001 From: Pau Date: Tue, 1 Apr 2025 20:38:07 +0200 Subject: [PATCH] added authorization classes to test endpoints based in permissions --- src/middlewares/auth.js | 104 +++++++++++++++++++++ src/services/authService.js | 59 ++++++++++++ src/utils/tokenPermissionsContentFilter.js | 65 +++++++++++++ 3 files changed, 228 insertions(+) create mode 100644 src/middlewares/auth.js create mode 100644 src/services/authService.js create mode 100644 src/utils/tokenPermissionsContentFilter.js diff --git a/src/middlewares/auth.js b/src/middlewares/auth.js new file mode 100644 index 0000000..14cfc47 --- /dev/null +++ b/src/middlewares/auth.js @@ -0,0 +1,104 @@ +import { sendResponseAccessDenied } from '../utils/responses.js'; +import { checkPermission, getDataFromToken, generateAccessToken} from '../services/authService.js'; +import { getRolesHasPermissionsModel } from '../models/authorization/roles_has_permissionsModel.js'; +import mysql from '../adapters/mysql.js'; +import { error404, errorHandler } from '../utils/errors.js'; +import { noResults } from '../validators/result-validators.js'; + +const obtainToken = (req, res) => { + return new Promise((resolve, reject) => { + const token = req.header('Authorization')?.replace('Bearer ', ''); + if (token) { + resolve(token); + } else { + sendResponseAccessDenied(res, { message: 'No authorization provided. Access token required' }); + reject(new Error('No authorization provided')); + } + }); +}; + +const setToken = (result, req, res, next, config) => { + const { user, role } = result._data + const token = generateAccessToken({ + payload: { user, role }, + config + }) + next({ user: { ...result, token } }) +} + +const authenticateToken = (req, res, next) => { + obtainToken(req, res) + .then((token) => getDataFromToken(token)) + .then((decoded) => { + req.auth = decoded; + next(); + }) + .catch((error) => { + console.error('Error in authentication middleware:', error); + sendResponseAccessDenied(res, { message: 'Access denied. Invalid token.' }); + }); +}; + +const authorizePermission = (endpoint) => { + return (req, res, next, config) => { + obtainToken(req, res) + .then((token) => getDataFromToken(token)) //extract user data from the token + .then((decoded) => { + const roleName = decoded.payload.role + _getRolePermissionsByName(roleName, config) + .then((rolePermissions) => { + const action = req.method + //check if the user has the necessary permissions + return checkPermission(action, endpoint, rolePermissions) + .then(({ hasPermission }) => { + if (!hasPermission) { + sendResponseAccessDenied(res, { + message: `You don't have permission to ${action} on ${endpoint}` + }); + throw new Error(`Permission denied for ${action} on ${endpoint}`); + } + + //attach user and role to the request object + //req.auth.user = user; + //req.auth.role = role; + + next(); + }) + }) + }) + .catch((error) => { + console.error('Error in authorization middleware:', error); + sendResponseAccessDenied(res, { message: 'Authorization error', error: error.message }); + }); + }; +}; + +const _getRolePermissionsByName = (roleName, config) => { + const conn = mysql.start(config) + return getRolesHasPermissionsModel({ roleName, conn }) + .then((response) => { + if (noResults(response)) { + const err = error404() + const error = errorHandler(err, config.environment) + return sendResponseNotFound(res, error) + } + console.log(response) + return response.map(({ permission_action, permission_endpoint }) => ({ + permission_action, + permission_endpoint + })) + }) + .catch((err) => { + const error = errorHandler(err, config.environment) + res.status(error.code).json(error) + }) + .finally(() => { + mysql.end(conn) + }) +} + +export { + authenticateToken, + authorizePermission, + setToken +}; \ No newline at end of file diff --git a/src/services/authService.js b/src/services/authService.js new file mode 100644 index 0000000..5497b5b --- /dev/null +++ b/src/services/authService.js @@ -0,0 +1,59 @@ +import jwt from 'jsonwebtoken'; +import dotenv from 'dotenv'; + +dotenv.config(); + +/** + * Decodes and verifies a JWT token. + * @param {string} token - The JWT token to verify. + * @returns {Promise} - Resolves with the decoded token payload. +*/ +const getDataFromToken = (token) => { + return new Promise((resolve, reject) => { + const JWT_SECRET = process.env.JWT_SECRET; + jwt.verify(token, JWT_SECRET, (err, decoded) => { + err ? reject(err) : resolve(decoded); + }); + }); +}; + +/** + * Generates a new JWT token. + * @param {Object} payload - The payload to include in the token. + * @returns {string} - The generated JWT token. +*/ +const generateAccessToken = (payload) => { + const JWT_SECRET = process.env.JWT_SECRET; + const JWT_TIME = parseInt(process.env.JWT_TIME, 10); + + return jwt.sign(payload, JWT_SECRET, JWT_TIME ? { expiresIn: JWT_TIME } : {}); +}; + +/** + * Checks if a user has permission to perform an action on a resource. + * @param {string} userId - The ID of the user. + * @param {string} action - The action to check (e.g., GET, POST, PUT, DELETE). + * @param {string} endpoin - The endpoint of the attack + * @param {Array} userPermissions - The list of permissions assigned to the user. + * @returns {Promise} - Resolves with an object containing permission details. + */ +const checkPermission = (action, endpoint, userPermissions) => { + return new Promise((resolve, reject) => { + const hasPermission = userPermissions.some( + (permission) => + permission.permission_action === action && permission.permission_endpoint === endpoint + ); + + if (hasPermission) { + resolve({ hasPermission: true }); + } else { + reject(new Error(`Permission denied for ${action} on ${endpoint}`)); + } + }); +}; + +export { + getDataFromToken, + generateAccessToken, + checkPermission, +}; \ No newline at end of file diff --git a/src/utils/tokenPermissionsContentFilter.js b/src/utils/tokenPermissionsContentFilter.js new file mode 100644 index 0000000..b22b531 --- /dev/null +++ b/src/utils/tokenPermissionsContentFilter.js @@ -0,0 +1,65 @@ +import { sendErrorForbidden } from './errorUtils' + +// Simplified version focusing on roles instead of parties +const getPermittedBusinessUnits = (req) => { + // Instead of extracting from parties_accesses, + // extract from user roles or permissions + const userRoles = req.auth.user.roles || [] + + // Example: Map roles to allowed business units + const businessUnitsByRole = { + 'admin': ['all'], // Admin can access everything + 'manager': ['specific-business-unit-uuid'], // Managers might have limited access + 'viewer': ['read-only-business-unit-uuid'] + } + + // Collect business units based on user's roles + const allowedBusinessUnits = userRoles + .flatMap(role => businessUnitsByRole[role] || []) + .filter(bu => bu !== 'all') + + return allowedBusinessUnits.length ? allowedBusinessUnits.join(',') : null +} + +const getPermittedCustomers = (req) => { + // Similar approach for customers + const userRoles = req.auth.user.roles || [] + + const customersByRole = { + 'admin': ['all'], + 'manager': ['specific-customer-uuid'], + 'viewer': ['read-only-customer-uuid'] + } + + const allowedCustomers = userRoles + .flatMap(role => customersByRole[role] || []) + .filter(customer => customer !== 'all') + + return allowedCustomers.length + ? { mandatory: allowedCustomers[0] } + : null +} + +const prepareRequest = (req, type, customerResult, businessUnitResult) => { + return { + ...req[type], + ...(customerResult ? { customerUuid: customerResult.mandatory } : {}), + ...(businessUnitResult ? { businessUnitUuid: businessUnitResult } : {}) + } +} + +const tokenPermissionsContentFilter = (req, res, next, config) => { + try { + const businessUnitUuid = getPermittedBusinessUnits(req) + const customerUuidResult = getPermittedCustomers(req) + + req.body = prepareRequest(req, 'body', customerUuidResult, businessUnitUuid) + req.query = prepareRequest(req, 'query', customerUuidResult, businessUnitUuid) + + return next() + } catch { + sendErrorForbidden({ res, config }) + } +} + +export { tokenPermissionsContentFilter } \ No newline at end of file