UNPKG

@rbac/rbac

Version:

Blazing Fast, Zero dependency, Hierarchical Role-Based Access Control for Node.js

213 lines (212 loc) 7.81 kB
"use strict"; 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;