myacl
Version:
Access control list manager for Expressjs
257 lines (215 loc) • 6.38 kB
JavaScript
const yaml = require("js-yaml");
const { extname } = require("path");
const { readFileSync } = require("fs");
const assert = require("assert");
const objectPath = require("object-path").withInheritedProps;
const path = require("path");
/**
*
* @param {[String]} url [url string to be converted to]
* * @return {[Array]} [Array containing the url contents]
*/
const urlToArray = (url) => {
if (typeof url !== "string") {
throw new Error("Only string arguments are allowed");
}
return url.replace(/^\/+|\/+$/gm, "").split("/");
};
const stripQueryStrings = (url) => url.split(/[?#]/)[0];
/**
* deny method is used to buid the deny response Object
*
* @param {[String]} customMessage [The message you would like added to the response]
* @param {[Object]} response [The response object when access is denied]
*/
const deny = (customMessage, response) => {
let message = customMessage ? customMessage : "Unauthorized access";
if (response && typeof response === "object") {
return response;
}
return {
status: "Access denied",
success: false,
message: message,
};
};
/**
* This method is used to ensure the correct wild card is supplied
* @param {[String]} term
* @param {String} name
*/
const assertIsGlobOrArray = (term, name) => {
if (typeof term !== "string" && !Array.isArray(term)) {
throw new Error(`TypeError: ${name} should be a array or string`);
}
if (typeof term === "string" && term !== "*") {
throw new Error(
`DefinitionError: Unrecognised glob "${term}" , use "*" instead`
);
}
};
/**
* Reads the config file and returns its content
*
* @param {[String]} configFilePath [path to the location of the config file]
*/
const readConfigFile = (configFilePath) => {
if (typeof configFilePath !== "string") {
throw new Error("TypeError: Path must be a string. Received undefined");
}
let configBuffer;
try {
configBuffer = readFileSync(path.resolve(configFilePath), "utf8");
if (extname(configFilePath) === ".json") {
return JSON.parse(configBuffer);
}
return yaml.safeLoad(configBuffer);
} catch (error) {
throw Error(error);
}
};
/**
* Maps each policy to user group
* @param {[Array]} policies [Array of policies ]
*/
const mapPolicyToGroup = (policies) => {
if (!policies) {
return;
}
const mappedPolicies = new Map();
policies.forEach((policy) => {
assert.equal(typeof policy.group, "string");
assert.equal(Array.isArray(policy.permissions), true);
policy.permissions.forEach((permission) => {
assert(typeof permission.resource, "string");
assertIsGlobOrArray(permission.methods, "Methods");
if (permission.action !== "allow" && permission.action !== "deny") {
throw new Error('TypeError: action should be either "deny" or "allow"');
}
});
// Transform policies into a map
mappedPolicies.set(policy.group, policy.permissions);
});
return mappedPolicies;
};
/**
* Validates policies and ensure
* @param {[Array]} policies
*/
const validatePolicies = (policies) => {
if (!Array.isArray(policies)) {
throw new Error("TypeError: Expected Array but got " + typeof policies);
}
return mapPolicyToGroup(policies);
};
const createRegexFromResource = (resource) => {
if (resource.startsWith(":") || resource === "*") {
return ".*";
}
return `^${resource}$`;
};
const matchUrlToResource = (route, resource) => {
if (resource === "*") return true;
// create an array form both route url and resource
const routeArray = urlToArray(route);
const resourceArray = urlToArray(resource);
for (let key = 0; key < routeArray.length; key++) {
if (key >= resourceArray.length) return false;
if (resourceArray[key] === "*") return true;
if (!routeArray[key].match(createRegexFromResource(resourceArray[key])))
return false;
}
if (resourceArray.length > routeArray.length) {
return resourceArray[routeArray.length] == "*";
}
return true;
};
const getPrefix = (resource) => resource.slice(0, resource.length - 2);
const findPermissionForRoute = (route, method, prefix = "", policy) => {
// Strip query strings from route
route = stripQueryStrings(route);
for (let permission of policy) {
let resource = permission.resource;
// check if route prefix has been specified
if (prefix) {
resource = `${prefix}/${resource}`.replace(/\/+/g, "/");
}
if (permission.subRoutes && permission.resource !== "*") {
const currentPrefix = resource.endsWith("/*")
? getPrefix(resource)
: resource;
let currentPermission = findPermissionForRoute(
route,
method,
currentPrefix,
permission.subRoutes
);
if (currentPermission) {
return currentPermission;
}
}
if (matchUrlToResource(route, resource)) {
return permission;
}
}
};
const isAllowed = (method, permission) => {
const isGlobOrHasMethod =
permission.methods === "*" || permission.methods.includes(method);
switch (isGlobOrHasMethod) {
case true:
return permission.action === "allow" ? true : false;
default:
return permission.action === "allow" ? false : true;
}
};
const checkIfHasAccess = (
method,
res,
next,
permission,
customMessage,
response,
denyCallback
) => {
if (isAllowed(method, permission)) {
return next();
}
if (typeof denyCallback === "function") {
return denyCallback(req, res, next);
}
return res.status(403).json(deny(customMessage, response));
};
const findRoleFromRequest = (
req,
searchPath,
defaultRole,
decodedObjectName
) => {
if (searchPath && objectPath.get(req, searchPath)) {
return objectPath.get(req, searchPath);
}
if (decodedObjectName && objectPath.get(req, `${decodedObjectName}.role`)) {
return objectPath.get(req, `${decodedObjectName}.role`);
}
if (req.decoded && req.decoded.role) {
return objectPath.get(req, "decoded.role");
}
if (req.session && req.session.role) {
return objectPath.get(req, "session.role");
}
return defaultRole;
};
module.exports = {
urlToArray,
readConfigFile,
mapPolicyToGroup,
findRoleFromRequest,
findPermissionForRoute,
checkIfHasAccess,
isAllowed,
validatePolicies,
matchUrlToResource,
assertIsGlobOrArray,
deny,
};