UNPKG

casbin

Version:

An authorization library that supports access control models like ACL, RBAC, ABAC in Node.JS

648 lines (647 loc) 25.1 kB
"use strict"; // Copyright 2018 The Casbin Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. Object.defineProperty(exports, "__esModule", { value: true }); exports.CoreEnforcer = void 0; const expression_eval_1 = require("@casbin/expression-eval"); const effect_1 = require("./effect"); const model_1 = require("./model"); const rbac_1 = require("./rbac"); const enforceContext_1 = require("./enforceContext"); const util_1 = require("./util"); const log_1 = require("./log"); /** * CoreEnforcer defines the core functionality of an enforcer. */ class CoreEnforcer { constructor() { this.fm = model_1.FunctionMap.loadFunctionMap(); this.eft = new effect_1.DefaultEffector(); this.matcherMap = new Map(); this.defaultEnforceContext = new enforceContext_1.EnforceContext('r', 'p', 'e', 'm'); this.watcher = null; this.watcherEx = null; this.enabled = true; this.autoSave = true; this.autoBuildRoleLinks = true; this.autoNotifyWatcher = true; this.acceptJsonRequest = false; } /** * setFileSystem sets a file system to read the model file or the policy file. * @param fs {@link FileSystem} */ setFileSystem(fs) { this.fs = fs; } /** * getFileSystem gets the file system, */ getFileSystem() { return this.fs; } getExpression(asyncCompile, exp) { const matcherKey = `${asyncCompile ? 'ASYNC[' : 'SYNC['}${exp}]`; (0, expression_eval_1.addBinaryOp)('in', 1, util_1.customIn); let expression = this.matcherMap.get(matcherKey); if (!expression) { exp = (0, util_1.bracketCompatible)(exp); expression = asyncCompile ? (0, expression_eval_1.compileAsync)(exp) : (0, expression_eval_1.compile)(exp); this.matcherMap.set(matcherKey, expression); } return expression; } /** * loadModel reloads the model from the model CONF file. * Because the policy is attached to a model, * so the policy is invalidated and needs to be reloaded by calling LoadPolicy(). */ loadModel() { this.model = (0, model_1.newModelFromFile)(this.modelPath, this.fs); this.model.printModel(); } /** * getModel gets the current model. * * @return the model of the enforcer. */ getModel() { return this.model; } /** * setModel sets the current model. * * @param m the model. */ setModel(m) { this.model = m; } /** * getAdapter gets the current adapter. * * @return the adapter of the enforcer. */ getAdapter() { return this.adapter; } /** * setAdapter sets the current adapter. * * @param adapter the adapter. */ setAdapter(adapter) { this.adapter = adapter; } /** * setWatcher sets the current watcher. * * @param watcher the watcher. */ setWatcher(watcher) { this.watcher = watcher; watcher.setUpdateCallback(async () => await this.loadPolicy()); } /** * setWatcherEx sets the current watcherEx. * * @param watcherEx the watcherEx. */ setWatcherEx(watcherEx) { this.watcherEx = watcherEx; } /** * setRoleManager sets the current role manager. * * @param rm the role manager. */ setRoleManager(rm) { this.rmMap.set('g', rm); } /** * setRoleManager sets the role manager for the named policy. * * @param ptype the named policy. * @param rm the role manager. */ setNamedRoleManager(ptype, rm) { this.rmMap.set(ptype, rm); } /** * getRoleManager gets the current role manager. */ getRoleManager() { return this.rmMap.get('g'); } /** * getNamedRoleManager gets role manager by name. */ getNamedRoleManager(name) { return this.rmMap.get(name); } /** * setEffector sets the current effector. * * @param eft the effector. */ setEffector(eft) { this.eft = eft; } /** * clearPolicy clears all policy. */ clearPolicy() { this.model.clearPolicy(); } initRmMap() { this.rmMap = new Map(); const rm = this.model.model.get('g'); if (rm) { for (const ptype of rm.keys()) { this.rmMap.set(ptype, new rbac_1.DefaultRoleManager(10)); } } } sortPolicies() { var _a; (_a = this.model.model.get('p')) === null || _a === void 0 ? void 0 : _a.forEach((value, key) => { const policy = value.policy; const tokens = value.tokens; if (policy && tokens) { const priorityIndex = tokens.indexOf(`${key}_priority`); if (priorityIndex !== -1) { policy.sort((a, b) => { return parseInt(a[priorityIndex], 10) - parseInt(b[priorityIndex], 10); }); } } }); } /** * loadPolicy reloads the policy from file/database. */ async loadPolicy() { this.model.clearPolicy(); await this.adapter.loadPolicy(this.model); this.sortPolicies(); this.model.sortPoliciesBySubjectHierarchy(); if (this.autoBuildRoleLinks) { await this.buildRoleLinksInternal(); } } /** * loadFilteredPolicy reloads a filtered policy from file/database. * * @param filter the filter used to specify which type of policy should be loaded. */ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async loadFilteredPolicy(filter) { this.model.clearPolicy(); this.sortPolicies(); this.model.sortPoliciesBySubjectHierarchy(); return this.loadIncrementalFilteredPolicy(filter); } /** * LoadIncrementalFilteredPolicy append a filtered policy from file/database. * * @param filter the filter used to specify which type of policy should be appended. */ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async loadIncrementalFilteredPolicy(filter) { if ('isFiltered' in this.adapter) { await this.adapter.loadFilteredPolicy(this.model, filter); } else { throw new Error('filtered policies are not supported by this adapter'); } this.sortPolicies(); if (this.autoBuildRoleLinks) { await this.buildRoleLinksInternal(); } return true; } /** * isFiltered returns true if the loaded policy has been filtered. * * @return if the loaded policy has been filtered. */ isFiltered() { if ('isFiltered' in this.adapter) { return this.adapter.isFiltered(); } return false; } /** * savePolicy saves the current policy (usually after changed with * Casbin API) back to file/database. */ async savePolicy() { if (this.isFiltered()) { throw new Error('Cannot save a filtered policy'); } const flag = await this.adapter.savePolicy(this.model); if (!flag) { return false; } if (this.watcherEx) { return await this.watcherEx.updateForSavePolicy(this.model); } else if (this.watcher) { return await this.watcher.update(); } return true; } /** * enableEnforce changes the enforcing state of Casbin, when Casbin is * disabled, all access will be allowed by the enforce() function. * * @param enable whether to enable the enforcer. */ enableEnforce(enable) { this.enabled = enable; } /** * enableLog changes whether to print Casbin log to the standard output. * * @param enable whether to enable Casbin's log. */ enableLog(enable) { (0, log_1.getLogger)().enableLog(enable); } /** * enableAutoSave controls whether to save a policy rule automatically to * the adapter when it is added or removed. * * @param autoSave whether to enable the AutoSave feature. */ enableAutoSave(autoSave) { this.autoSave = autoSave; } /** * enableAutoNotifyWatcher controls whether to save a policy rule automatically notify the Watcher when it is added or removed. * @param enable whether to enable the AutoNotifyWatcher feature. */ enableAutoNotifyWatcher(enable) { this.autoNotifyWatcher = enable; } /** * enableAcceptJsonRequest determines whether to attempt parsing request args as JSON * * @param enable whether to attempt parsing request args as JSON */ enableAcceptJsonRequest(enable) { this.acceptJsonRequest = enable; } /** * enableAutoBuildRoleLinks controls whether to save a policy rule * automatically to the adapter when it is added or removed. * * @param autoBuildRoleLinks whether to automatically build the role links. */ enableAutoBuildRoleLinks(autoBuildRoleLinks) { this.autoBuildRoleLinks = autoBuildRoleLinks; } /** * add matching function to RoleManager by ptype * @param ptype g * @param fn the function will be added */ async addNamedMatchingFunc(ptype, fn) { const rm = this.rmMap.get(ptype); if (rm) { return await rm.addMatchingFunc(fn); } throw Error('Target ptype not found.'); } /** * add domain matching function to RoleManager by ptype * @param ptype g * @param fn the function will be added */ async addNamedDomainMatchingFunc(ptype, fn) { const rm = this.rmMap.get(ptype); if (rm) { return await rm.addDomainMatchingFunc(fn); } } /** * buildRoleLinks manually rebuild the role inheritance relations. */ async buildRoleLinks() { return this.buildRoleLinksInternal(); } /** * buildIncrementalRoleLinks provides incremental build the role inheritance relations. * @param op policy operation * @param ptype g * @param rules policies */ async buildIncrementalRoleLinks(op, ptype, rules) { let rm = this.rmMap.get(ptype); if (!rm) { rm = new rbac_1.DefaultRoleManager(10); this.rmMap.set(ptype, rm); } await this.model.buildIncrementalRoleLinks(rm, op, 'g', ptype, rules); } async buildRoleLinksInternal() { for (const rm of this.rmMap.values()) { await rm.clear(); await this.model.buildRoleLinks(this.rmMap); } } *privateEnforce(asyncCompile = true, explain = false, matcher, enforceContext = new enforceContext_1.EnforceContext('r', 'p', 'e', 'm'), ...rvals) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; if (!this.enabled) { return true; } let explainIndex = -1; const functions = {}; this.fm.getFunctions().forEach((value, key) => { functions[key] = value; }); const astMap = this.model.model.get('g'); astMap === null || astMap === void 0 ? void 0 : astMap.forEach((value, key) => { const rm = value.rm; functions[key] = asyncCompile ? (0, util_1.generateGFunction)(rm) : (0, util_1.generateSyncedGFunction)(rm); }); let expString; if (!matcher) { expString = (_b = (_a = this.model.model.get('m')) === null || _a === void 0 ? void 0 : _a.get(enforceContext.mType)) === null || _b === void 0 ? void 0 : _b.value; } else { expString = (0, util_1.removeComments)((0, util_1.escapeAssertion)(matcher)); } if (!expString) { throw new Error('Unable to find matchers in model'); } const effectExpr = (_d = (_c = this.model.model.get('e')) === null || _c === void 0 ? void 0 : _c.get(enforceContext.eType)) === null || _d === void 0 ? void 0 : _d.value; if (!effectExpr) { throw new Error('Unable to find policy_effect in model'); } const HasEval = (0, util_1.hasEval)(expString); let expression = undefined; const p = (_e = this.model.model.get('p')) === null || _e === void 0 ? void 0 : _e.get(enforceContext.pType); const policyLen = (_f = p === null || p === void 0 ? void 0 : p.policy) === null || _f === void 0 ? void 0 : _f.length; const rTokens = (_h = (_g = this.model.model.get('r')) === null || _g === void 0 ? void 0 : _g.get(enforceContext.rType)) === null || _h === void 0 ? void 0 : _h.tokens; const rTokensLen = rTokens === null || rTokens === void 0 ? void 0 : rTokens.length; const effectStream = this.eft.newStream(effectExpr); if (policyLen && policyLen !== 0 && expString.includes(`${enforceContext.pType}_`)) { for (let i = 0; i < policyLen; i++) { const parameters = {}; if ((rTokens === null || rTokens === void 0 ? void 0 : rTokens.length) !== rvals.length) { throw new Error(`invalid request size: expected ${rTokensLen}, got ${rvals.length}, rvals: ${rvals}"`); } if (this.acceptJsonRequest) { // Attempt to parse each request parameter as JSON; continue with string if failed rTokens.forEach((token, j) => { try { parameters[token] = JSON.parse(rvals[j]); } catch (_a) { parameters[token] = rvals[j]; } }); } else { rTokens.forEach((token, j) => { parameters[token] = rvals[j]; }); } p === null || p === void 0 ? void 0 : p.tokens.forEach((token, j) => { parameters[token] = p === null || p === void 0 ? void 0 : p.policy[i][j]; }); if (HasEval) { const ruleNames = (0, util_1.getEvalValue)(expString); let expWithRule = expString; for (const ruleName of ruleNames) { if (ruleName in parameters) { const rule = (0, util_1.escapeAssertion)(parameters[ruleName]); expWithRule = (0, util_1.replaceEval)(expWithRule, ruleName, rule); } else { throw new Error(`${ruleName} not in ${parameters}`); } } expression = this.getExpression(asyncCompile, expWithRule); } else { if (expression === undefined) { expression = this.getExpression(asyncCompile, expString); } } const context = Object.assign(Object.assign({}, parameters), functions); const result = asyncCompile ? yield expression(context) : expression(context); let eftRes; switch (typeof result) { case 'boolean': eftRes = result ? effect_1.Effect.Allow : effect_1.Effect.Indeterminate; break; case 'number': if (result === 0) { eftRes = effect_1.Effect.Indeterminate; } else { eftRes = result; } break; case 'string': if (result === '') { eftRes = effect_1.Effect.Indeterminate; } else { eftRes = effect_1.Effect.Allow; } break; default: throw new Error('matcher result should only be of type boolean, number, or string'); } const eft = parameters[`${enforceContext.pType}_eft`]; if (eft && eftRes === effect_1.Effect.Allow) { if (eft === 'allow') { eftRes = effect_1.Effect.Allow; } else if (eft === 'deny') { eftRes = effect_1.Effect.Deny; } else { eftRes = effect_1.Effect.Indeterminate; } } const [res, rec, done] = effectStream.pushEffect(eftRes); if (rec) { explainIndex = i; } if (done) { break; } } } else { explainIndex = 0; const parameters = {}; rTokens === null || rTokens === void 0 ? void 0 : rTokens.forEach((token, j) => { parameters[token] = rvals[j]; }); (_j = p === null || p === void 0 ? void 0 : p.tokens) === null || _j === void 0 ? void 0 : _j.forEach((token) => { parameters[token] = ''; }); expression = this.getExpression(asyncCompile, expString); const context = Object.assign(Object.assign({}, parameters), functions); const result = asyncCompile ? yield expression(context) : expression(context); if (result) { effectStream.pushEffect(effect_1.Effect.Allow); } else { effectStream.pushEffect(effect_1.Effect.Indeterminate); } } const res = effectStream.current(); // only generate the request --> result string if the message // is going to be logged. if ((0, log_1.getLogger)().isEnable()) { let reqStr = 'Request: '; for (let i = 0; i < rvals.length; i++) { if (i !== rvals.length - 1) { reqStr += `${rvals[i]}, `; } else { reqStr += rvals[i]; } } reqStr += ` ---> ${res}`; (0, log_1.logPrint)(reqStr); } if (explain) { if (explainIndex === -1) { return [res, []]; } return [res, ((_k = p === null || p === void 0 ? void 0 : p.policy) === null || _k === void 0 ? void 0 : _k[explainIndex]) || []]; } return res; } /** * If the matchers does not contain an asynchronous method, call it faster. * * enforceSync decides whether a "subject" can access a "object" with * the operation "action", input parameters are usually: (sub, obj, act). * * @param rvals the request needs to be mediated, usually an array * of strings, can be class instances if ABAC is used. * @return whether to allow the request. */ enforceSync(...rvals) { if (rvals[0] instanceof enforceContext_1.EnforceContext) { const enforceContext = rvals.shift(); return (0, util_1.generatorRunSync)(this.privateEnforce(false, false, '', enforceContext, ...rvals)); } return (0, util_1.generatorRunSync)(this.privateEnforce(false, false, '', this.defaultEnforceContext, ...rvals)); } /** * If the matchers does not contain an asynchronous method, call it faster. * * enforceSync decides whether a "subject" can access a "object" with * the operation "action", input parameters are usually: (sub, obj, act). * * @param rvals the request needs to be mediated, usually an array * of strings, can be class instances if ABAC is used. * @return whether to allow the request and the reason rule. */ enforceExSync(...rvals) { if (rvals[0] instanceof enforceContext_1.EnforceContext) { const enforceContext = rvals.shift(); return (0, util_1.generatorRunSync)(this.privateEnforce(false, true, '', enforceContext, ...rvals)); } return (0, util_1.generatorRunSync)(this.privateEnforce(false, true, '', this.defaultEnforceContext, ...rvals)); } /** * Same as enforceSync. To be removed. */ enforceWithSyncCompile(...rvals) { return this.enforceSync(...rvals); } /** * enforce decides whether a "subject" can access a "object" with * the operation "action", input parameters are usually: (sub, obj, act). * * @param rvals the request needs to be mediated, usually an array * of strings, can be class instances if ABAC is used. * @return whether to allow the request. */ async enforce(...rvals) { if (rvals[0] instanceof enforceContext_1.EnforceContext) { const enforceContext = rvals.shift(); return (0, util_1.generatorRunAsync)(this.privateEnforce(true, false, '', enforceContext, ...rvals)); } return (0, util_1.generatorRunAsync)(this.privateEnforce(true, false, '', this.defaultEnforceContext, ...rvals)); } /** * enforceWithMatcher decides whether a "subject" can access a "object" with * the operation "action" but with the matcher passed, * input parameters are usually: (matcher, sub, obj, act). * * @param matcher matcher string. * @param rvals the request needs to be mediated, usually an array * of strings, can be class instances if ABAC is used. * @return whether to allow the request. */ async enforceWithMatcher(matcher, ...rvals) { if (rvals[0] instanceof enforceContext_1.EnforceContext) { const enforceContext = rvals.shift(); return (0, util_1.generatorRunAsync)(this.privateEnforce(true, false, matcher, enforceContext, ...rvals)); } return (0, util_1.generatorRunAsync)(this.privateEnforce(true, false, matcher, this.defaultEnforceContext, ...rvals)); } /** * enforce decides whether a "subject" can access a "object" with * the operation "action", input parameters are usually: (sub, obj, act). * * @param rvals the request needs to be mediated, usually an array * of strings, can be class instances if ABAC is used. * @return whether to allow the request and the reason rule. */ async enforceEx(...rvals) { if (rvals[0] instanceof enforceContext_1.EnforceContext) { const enforceContext = rvals.shift(); return (0, util_1.generatorRunAsync)(this.privateEnforce(true, true, '', enforceContext, ...rvals)); } return (0, util_1.generatorRunAsync)(this.privateEnforce(true, true, '', this.defaultEnforceContext, ...rvals)); } /** * enforceExWithMatcher decides whether a "subject" can access a "object" with * the operation "action" but with the matcher passed, * input parameters are usually: (matcher, sub, obj, act). * * @param matcher matcher string. * @param rvals the request needs to be mediated, usually an array * of strings, can be class instances if ABAC is used. * @return whether to allow the request and the reason rule. */ async enforceExWithMatcher(matcher, ...rvals) { if (rvals[0] instanceof enforceContext_1.EnforceContext) { const enforceContext = rvals.shift(); return (0, util_1.generatorRunAsync)(this.privateEnforce(true, true, matcher, enforceContext, ...rvals)); } return (0, util_1.generatorRunAsync)(this.privateEnforce(true, true, matcher, this.defaultEnforceContext, ...rvals)); } /** * batchEnforce enforces each request and returns result in a bool array. * @param rvals the request need to be mediated, usually an array * of array of strings, can be class instances if ABAC is used. * @returns whether to allow the requests. */ async batchEnforce(rvals) { return await Promise.all(rvals.map((rval) => this.enforce(...rval))); } } exports.CoreEnforcer = CoreEnforcer;