@rbac/rbac
Version:
Blazing Fast, Zero dependency, Hierarchical Role-Based Access Control for Node.js
213 lines (212 loc) • 7.81 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const helpers_1 = require("./helpers");
const cacheAndReturn = (cache, key, value) => {
cache.set(key, value);
return value;
};
const can = (config = { logger: helpers_1.defaultLogger, enableLogger: true }) => (mappedRoles, matchCache) => {
const logger = config.logger || helpers_1.defaultLogger;
const patternMatchCache = new Map();
const operationHintCache = new Map();
const log = config.enableLogger
? (roleName, operation, result, enabled) => {
if (enabled)
logger(roleName, operation, result, config.colors);
return result;
}
: (_r, _o, result) => result;
const getRoleCache = (roleName) => {
let cached = matchCache.get(roleName);
if (!cached) {
cached = new Map();
matchCache.set(roleName, cached);
}
return cached;
};
const getPatternCache = (roleName) => {
let cached = patternMatchCache.get(roleName);
if (!cached) {
cached = new Map();
patternMatchCache.set(roleName, cached);
}
return cached;
};
const getOperationHint = (op) => {
let hint = operationHintCache.get(op);
if (hint)
return hint;
const regex = (0, helpers_1.regexFromOperation)(op);
const isGlobOperation = !regex ? (0, helpers_1.isGlob)(op) : false;
hint = { regex, isGlob: isGlobOperation };
operationHintCache.set(op, hint);
return hint;
};
const checkDirect = async (logRole, resolvedRole, operation, params, logEnabled = true, skipFalseLog = false) => {
let whenFn;
if (typeof operation === 'string') {
if (resolvedRole.direct.has(operation)) {
return log(logRole, operation, true, logEnabled);
}
whenFn = resolvedRole.conditional.get(operation);
}
let regexOperation = null;
let isGlobOperation = false;
if (operation instanceof RegExp) {
regexOperation = operation;
}
else if (typeof operation === 'string') {
const hint = getOperationHint(operation);
regexOperation = hint.regex;
isGlobOperation = hint.isGlob;
}
if (regexOperation || isGlobOperation) {
const regex = isGlobOperation
? (0, helpers_1.globToRegex)(operation)
: regexOperation;
const cacheKey = isGlobOperation
? `glob:${operation}`
: `regex:${regex.toString()}`;
const cache = getRoleCache(logRole);
const cached = cache.get(cacheKey);
if (cached !== undefined) {
return log(logRole, operation, cached, logEnabled);
}
return log(logRole, operation, cacheAndReturn(cache, cacheKey, (0, helpers_1.hasMatchingOperation)(regex, resolvedRole.allOps)), logEnabled);
}
if (!whenFn) {
const operationString = typeof operation === 'string' ? operation : String(operation);
const patternCache = getPatternCache(logRole);
let cachedWhen = patternCache.get(operationString);
if (cachedWhen === undefined) {
const matchPattern = resolvedRole.patterns.find(p => p.regex.test(operationString));
cachedWhen = matchPattern ? matchPattern.when : null;
patternCache.set(operationString, cachedWhen);
}
if (cachedWhen)
whenFn = cachedWhen;
}
if (!whenFn) {
if (!skipFalseLog)
log(logRole, operation, false, logEnabled);
return false;
}
return evaluateWhen(whenFn);
async function evaluateWhen(when) {
if (when === true) {
log(logRole, operation, true, logEnabled);
return true;
}
if (!when) {
if (!skipFalseLog)
log(logRole, operation, false, logEnabled);
return false;
}
try {
const res = await when(params);
log(logRole, operation, res, logEnabled);
return res;
}
catch (_a) {
log(logRole, operation, false, logEnabled);
return false;
}
}
};
const check = async (role, operation, params, logEnabled = true) => {
const resolvedRole = mappedRoles[role];
if (!resolvedRole) {
return log(role, operation, false, logEnabled);
}
return checkDirect(role, resolvedRole, operation, params, logEnabled);
};
return (role, operation, params) => check(role, operation, params);
};
const flattenRoles = (roles) => {
const memo = {};
const visit = (name, stack) => {
if (memo[name])
return memo[name];
if (stack.has(name))
return {
direct: new Set(),
conditional: new Map(),
patterns: [],
allOps: []
};
stack.add(name);
const role = roles[name];
let direct = new Set();
let conditional = new Map();
let patterns = [];
let inherits;
let all = [];
if (role) {
if (role.inherits) {
inherits = role.inherits;
for (const parent of role.inherits) {
const parentRole = visit(parent, stack);
for (const op of parentRole.direct)
direct.add(op);
for (const [k, v] of parentRole.conditional)
conditional.set(k, v);
patterns.push(...parentRole.patterns);
all = Array.from(new Set(all.concat(parentRole.allOps)));
}
}
const built = (0, helpers_1.buildPermissionData)(role.can);
for (const op of built.direct)
direct.add(op);
for (const [k, v] of built.conditional)
conditional.set(k, v);
patterns.push(...built.patterns);
all = Array.from(new Set(all.concat(built.all)));
}
stack.delete(name);
const seen = new Set();
const unique = [];
for (const p of patterns) {
const key = p.name + p.regex.source;
if (!seen.has(key)) {
seen.add(key);
unique.push(p);
}
}
const mapped = {
direct,
conditional,
patterns: unique,
inherits,
allOps: Array.from(new Set([...direct, ...conditional.keys(), ...unique.map(p => p.name)]))
};
memo[name] = mapped;
return mapped;
};
for (const name of Object.keys(roles)) {
visit(name, new Set());
}
return memo;
};
const RBAC = (config = {}) => (roles) => {
let allRoles = { ...roles };
let mappedRoles = flattenRoles(allRoles);
const matchCache = new Map();
const checker = can(config);
const canFn = (role, operation, params) => checker(mappedRoles, matchCache)(role, operation, params);
const updateRoles = (newRoles) => {
allRoles = { ...allRoles, ...newRoles };
mappedRoles = flattenRoles(allRoles);
matchCache.clear();
};
const addRole = (roleName, roleDef) => {
allRoles = { ...allRoles, [roleName]: roleDef };
mappedRoles = flattenRoles(allRoles);
matchCache.clear();
};
return {
can: canFn,
updateRoles,
addRole
};
};
exports.default = RBAC;