|
// -------------------------------------------- |
|
// Example: |
|
// -------------------------------------------- |
|
// const roles = { |
|
// admin: { |
|
// can: ['posts:c', 'posts:r', 'posts:u', 'posts:d', 'comments:c', 'comments:r', 'comments:u', 'comments:d'], |
|
// }, |
|
// editor: { |
|
// can: ['posts:r', 'posts:u', 'comments:r', 'comments:u'], |
|
// }, |
|
// user: { |
|
// can: ['posts:r', 'comments:r'], |
|
// }, |
|
// guest: { |
|
// can: ['posts:r'], |
|
// }, |
|
// }; |
|
|
|
const assert = require("assert"); |
|
|
|
/** |
|
* Normalize function converts a values parameter into a normalized string. |
|
* @example |
|
* normalize( 'ud' ) = " ud" |
|
* normalize( ['u','d'] ) = " ud" |
|
* normalize( 'c' ) = "c " |
|
* normalize( ['c'] ) = "c " |
|
* normalize( 'dlaad' ) = " d" |
|
* normalize( ['d','l','a','a','d'] ) = " d" |
|
* normalize( 'crud' ) = "crud" |
|
* normalize( ['c','r','u','d'] ) = "crud" |
|
* normalize( 'durc' ) = "crud" |
|
* normalize( ['d','u','r','c'] ) = "crud" |
|
* normalize( 'nope' ) = " " |
|
* normalize( ['n','o','p','e'] ) = " " |
|
* |
|
* @param {String|Array} values - The values to be normalized. Can be a string or an array of strings. |
|
* @returns {String} - A normalized string consisting of 'crud' characters. |
|
* @throws {Error} - Throws an error if the values parameter is neither a string nor an array of strings. |
|
*/ |
|
const normalize = (values) => { |
|
let arrValues = []; |
|
if (Array.isArray(values)) { |
|
arrValues = values; |
|
} else if (typeof values === 'string') { |
|
arrValues = values.split(''); |
|
} else { |
|
throw new Error('normalize only supports a String, or an Array of Strings') |
|
} |
|
const clean = [...new Set(arrValues.map(s => s && s.toLowerCase()))]; |
|
const result = [' ', ' ', ' ', ' ']; |
|
const order = ['c', 'r', 'u', 'd']; |
|
order.forEach((val, i) => { |
|
if(clean.includes(val)) { |
|
result[i] = val; |
|
} |
|
}); |
|
// results in a string 'crud', ' ', 'c d', or some mix of crud |
|
return result.join(''); |
|
} |
|
|
|
async function authenticate(req, res, next) { |
|
// -------------------------------------------- |
|
// Note: |
|
// -------------------------------------------- |
|
// This is where you would extract the identifier, |
|
// which could be a JWT, Basic Auth token, or |
|
// something similar. For this example, I've just |
|
// placed a user ID in the request object. |
|
// -------------------------------------------- |
|
try { |
|
req.context = req.context ?? {}; |
|
// -------------------------------------------- |
|
// Note: |
|
// -------------------------------------------- |
|
// Users can have multiple Roles as needed. |
|
// |
|
// Permissions are attached to Roles, not |
|
// directly to the user. |
|
// |
|
// Permissions are additive in nature, meaning |
|
// if a user has two roles—one with a permission |
|
// and the other without—the user is granted |
|
// access because the permission is added to |
|
// the user. |
|
// |
|
// This approach to handling permissions is |
|
// commonly referred to as |
|
// "role-based access control" (RBAC). |
|
// |
|
// Avoid storing the user's roles or permissions |
|
// in the session if possible. The downside |
|
// of doing this is that if roles or permissions |
|
// change, the user would need to log out |
|
// and log back in to see the updates. |
|
// |
|
// Storing values as arrays, in JavaScript, |
|
// allows us to use Array.prototype functions |
|
// such as includes, every, includes, and some |
|
// -------------------------------------------- |
|
switch (req.user?.username) { |
|
case '10': |
|
req.context.user = { |
|
id: 10, |
|
username: 'Rick Shaw', |
|
roles: Array.from(new Set(['admin',])), |
|
// ["posts:crud", "comments:crud"] |
|
permissions: Array.from(new Set([`posts:${normalize(['c', 'r', 'u', 'd'])}`, `comments:${normalize(['c', 'r', 'u', 'd'])}`])), |
|
} |
|
break; |
|
case '100': |
|
req.context.user = { |
|
id: 100, |
|
username: 'X. Benedict', |
|
roles: Array.from(new Set(['editor',])), |
|
// ["posts:_ru_", "comments:_ru_"] |
|
permissions: Array.from(new Set([`posts:${normalize(['r', 'u'])}`, `comments:${normalize(['r', 'u'])}`])), |
|
} |
|
break; |
|
default: |
|
console.error(`User not found for ${req.user?.id}`); |
|
return res.status(403).json({message: 'Forbidden'}); |
|
} |
|
next(); |
|
} catch (err) { |
|
console.error(err); |
|
res.status(500).json({message: 'Internal Server Error'}); |
|
} |
|
} |
|
|
|
/** |
|
* Middleware to guard route access based on user permissions. |
|
* |
|
* @param {string} resource - The resource being accessed. |
|
* @param {string} action - The action to be performed on the resource. |
|
* @returns {function} - Returns express middleware function. |
|
*/ |
|
function authorize(resource, action) { |
|
/** |
|
* Middleware function to guard routes based on user permissions. |
|
* |
|
* @param {object} req - The request object. |
|
* @param {object} res - The response object. |
|
* @param {function} next - The next middleware function. |
|
* @returns {undefined} - Returns undefined. |
|
*/ |
|
return (req, res, next) => { |
|
assert(req.context?.user, 'Context is required') |
|
const user = req.context.user; |
|
|
|
if (user.permissions.some((userPermission)=>{ |
|
const [userResource,userActions] = userPermission.split(':') |
|
if ( userResource === resource ) { |
|
return userActions.split('').includes(action) |
|
} |
|
return false |
|
})) { |
|
return next() |
|
} else { |
|
return res.status(403).json({message: 'Forbidden'}); |
|
} |
|
}; |
|
} |
|
|
|
module.exports = {authorize, authenticate} |