UNPKG

casbin

Version:

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

558 lines (557 loc) 19.4 kB
// 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. import * as util from '../util'; import { Config } from '../config'; import { Assertion } from './assertion'; import { getLogger, logPrint } from '../log'; import { DefaultRoleManager } from '../rbac'; const defaultDomain = ''; const defaultSeparator = '::'; export const sectionNameMap = { r: 'request_definition', p: 'policy_definition', g: 'role_definition', e: 'policy_effect', m: 'matchers', }; export var PolicyOp; (function (PolicyOp) { PolicyOp[PolicyOp["PolicyAdd"] = 0] = "PolicyAdd"; PolicyOp[PolicyOp["PolicyRemove"] = 1] = "PolicyRemove"; })(PolicyOp || (PolicyOp = {})); export const requiredSections = ['r', 'p', 'e', 'm']; export class Model { /** * constructor is the constructor for Model. */ constructor() { this.model = new Map(); } loadAssertion(cfg, sec, key) { const secName = sectionNameMap[sec]; const value = cfg.getString(`${secName}::${key}`); return this.addDef(sec, key, value); } getKeySuffix(i) { if (i === 1) { return ''; } return i.toString(); } loadSection(cfg, sec) { let i = 1; for (;;) { if (!this.loadAssertion(cfg, sec, sec + this.getKeySuffix(i))) { break; } else { i++; } } } // addDef adds an assertion to the model. addDef(sec, key, value) { if (value === '') { return false; } const ast = new Assertion(); ast.key = key; ast.value = value; ast.fieldIndexMap = new Map(); if (sec === 'r' || sec === 'p') { const tokens = value.split(',').map((n) => n.trim()); for (let i = 0; i < tokens.length; i++) { tokens[i] = key + '_' + tokens[i]; } ast.tokens = tokens; } else if (sec === 'm') { const stringArguments = value.match(/\"(.*?)\"/g) || []; stringArguments.forEach((n, index) => { value = value.replace(n, `$<${index}>`); }); value = util.escapeAssertion(value); stringArguments.forEach((n, index) => { value = value.replace(`$<${index}>`, n); }); const invalidOperators = /(?<![&|])&(?!&)|(?<![&|])\|(?!\|)|&{3,}|\|{3,}/g; if (invalidOperators.test(value)) { throw new Error(`Invalid operator in matcher`); } ast.value = value; } else { ast.value = util.escapeAssertion(value); } const nodeMap = this.model.get(sec); if (nodeMap) { nodeMap.set(key, ast); } else { const assertionMap = new Map(); assertionMap.set(key, ast); this.model.set(sec, assertionMap); } return true; } /** * loadModel loads the model from model CONF file. * @param path the model file path * @param fs {@link FileSystem} * @deprecated {@link loadModelFromFile} */ loadModel(path, fs) { this.loadModelFromFile(path, fs); } /** * loadModelFromFile loads the model from model CONF file. * @param path the model file path * @param fs {@link FileSystem} */ loadModelFromFile(path, fs) { const cfg = Config.newConfigFromFile(path, fs); this.loadModelFromConfig(cfg); } // loadModelFromText loads the model from the text. loadModelFromText(text) { const cfg = Config.newConfigFromText(text); this.loadModelFromConfig(cfg); } loadModelFromConfig(cfg) { for (const s in sectionNameMap) { this.loadSection(cfg, s); } const ms = []; requiredSections.forEach((n) => { if (!this.hasSection(n)) { ms.push(sectionNameMap[n]); } }); if (ms.length > 0) { throw new Error(`missing required sections: ${ms.join(',')}`); } } hasSection(sec) { return this.model.get(sec) !== undefined; } // printModel prints the model to the log. printModel() { logPrint('Model:'); this.model.forEach((value, key) => { value.forEach((ast, astKey) => { logPrint(`${key}.${astKey}: ${ast.value}`); }); }); } // buildIncrementalRoleLinks provides incremental build the role inheritance relations. async buildIncrementalRoleLinks(rm, op, sec, ptype, rules) { var _a, _b; if (sec === 'g') { await ((_b = (_a = this.model.get(sec)) === null || _a === void 0 ? void 0 : _a.get(ptype)) === null || _b === void 0 ? void 0 : _b.buildIncrementalRoleLinks(rm, op, rules)); } } // buildRoleLinks initializes the roles in RBAC. async buildRoleLinks(rmMap) { const astMap = this.model.get('g'); if (!astMap) { return; } for (const key of astMap.keys()) { const ast = astMap.get(key); let rm = rmMap.get(key); if (!rm) { rm = new DefaultRoleManager(10); rmMap.set(key, rm); } await (ast === null || ast === void 0 ? void 0 : ast.buildRoleLinks(rm)); } } // clearPolicy clears all current policy. clearPolicy() { this.model.forEach((value, key) => { if (key === 'p' || key === 'g') { value.forEach((ast) => { ast.policy = []; }); } }); } // getPolicy gets all rules in a policy. getPolicy(sec, key) { var _a; const policy = []; const ast = (_a = this.model.get(sec)) === null || _a === void 0 ? void 0 : _a.get(key); if (ast) { policy.push(...ast.policy); } return policy; } // hasPolicy determines whether a model has the specified policy rule. hasPolicy(sec, key, rule) { var _a; const ast = (_a = this.model.get(sec)) === null || _a === void 0 ? void 0 : _a.get(key); if (!ast) { return false; } return ast.policy.some((n) => util.arrayEquals(n, rule)); } // addPolicy adds a policy rule to the model. addPolicy(sec, key, rule) { var _a; if (!this.hasPolicy(sec, key, rule)) { const ast = (_a = this.model.get(sec)) === null || _a === void 0 ? void 0 : _a.get(key); if (!ast) { return false; } const policy = ast.policy; const tokens = ast.tokens; const priorityIndex = tokens.indexOf(`${key}_priority`); if (priorityIndex !== -1) { const priorityRule = rule[priorityIndex]; const insertIndex = policy.findIndex((oneRule) => oneRule[priorityIndex] >= priorityRule); if (priorityIndex === -1) { policy.push(rule); } else { policy.splice(insertIndex, 0, rule); } } else { policy.push(rule); } return true; } return false; } // addPolicies adds policy rules to the model. addPolicies(sec, ptype, rules) { var _a; const ast = (_a = this.model.get(sec)) === null || _a === void 0 ? void 0 : _a.get(ptype); if (!ast) { return [false, []]; } for (const rule of rules) { if (this.hasPolicy(sec, ptype, rule)) { return [false, []]; } } const priorityFlag = ast.tokens.indexOf(`${ptype}_priority`) !== -1; if (priorityFlag) { rules.forEach((rule) => { this.addPolicy(sec, ptype, rule); }); } else { ast.policy = ast.policy.concat(rules); } return [true, rules]; } // updatePolicy updates a policy from the model updatePolicy(sec, ptype, oldRule, newRule) { var _a; const ast = (_a = this.model.get(sec)) === null || _a === void 0 ? void 0 : _a.get(ptype); if (!ast) { return false; } const index = ast.policy.findIndex((r) => util.arrayEquals(r, oldRule)); if (index === -1) { return false; } const priorityIndex = ast.tokens.indexOf(`${ptype}_priority`); if (priorityIndex !== -1) { if (oldRule[priorityIndex] === newRule[priorityIndex]) { ast.policy[index] = newRule; } else { // this.removePolicy(sec, ptype, oldRule); // this.addPolicy(sec, ptype, newRule); throw new Error('new rule should have the same priority with old rule.'); } } else { ast.policy[index] = newRule; } return true; } // removePolicy removes a policy rule from the model. removePolicy(sec, key, rule) { var _a; if (this.hasPolicy(sec, key, rule)) { const ast = (_a = this.model.get(sec)) === null || _a === void 0 ? void 0 : _a.get(key); if (!ast) { return false; } ast.policy = ast.policy.filter((r) => !util.arrayEquals(rule, r)); return true; } return false; } // removePolicies removes policy rules from the model. removePolicies(sec, ptype, rules) { var _a; const effects = []; const ast = (_a = this.model.get(sec)) === null || _a === void 0 ? void 0 : _a.get(ptype); if (!ast) { return [false, []]; } for (const rule of rules) { if (!this.hasPolicy(sec, ptype, rule)) { return [false, []]; } } for (const rule of rules) { ast.policy = ast.policy.filter((r) => { const equals = util.arrayEquals(rule, r); if (equals) { effects.push(r); } return !equals; }); } return [true, effects]; } // getFilteredPolicy gets rules based on field filters from a policy. getFilteredPolicy(sec, key, fieldIndex, ...fieldValues) { var _a; const res = []; const ast = (_a = this.model.get(sec)) === null || _a === void 0 ? void 0 : _a.get(key); if (!ast) { return res; } for (const rule of ast.policy) { let matched = true; for (let i = 0; i < fieldValues.length; i++) { const fieldValue = fieldValues[i]; if (fieldValue !== '' && rule[fieldIndex + i] !== fieldValue) { matched = false; break; } } if (matched) { res.push(rule); } } return res; } // removeFilteredPolicy removes policy rules based on field filters from the model. removeFilteredPolicy(sec, key, fieldIndex, ...fieldValues) { var _a; const res = []; const effects = []; let bool = false; if (fieldValues.length === 0) { return [false, effects]; } const ast = (_a = this.model.get(sec)) === null || _a === void 0 ? void 0 : _a.get(key); if (!ast) { return [false, []]; } for (const rule of ast.policy) { let matched = true; for (let i = 0; i < fieldValues.length; i++) { const fieldValue = fieldValues[i]; if (fieldValue !== '' && rule[fieldIndex + i] !== fieldValue) { matched = false; break; } } if (matched) { bool = true; effects.push(rule); } else { res.push(rule); } } if (effects.length !== 0) { ast.policy = res; } return [bool, effects]; } // getValuesForFieldInPolicy gets all values for a field for all rules in a policy, duplicated values are removed. getValuesForFieldInPolicy(sec, key, fieldIndex) { var _a; const values = []; const ast = (_a = this.model.get(sec)) === null || _a === void 0 ? void 0 : _a.get(key); if (!ast) { return values; } return util.arrayRemoveDuplicates(ast.policy.map((n) => n[fieldIndex])); } // getValuesForFieldInPolicyAllTypes gets all values for a field for all rules in a policy of all ptypes, duplicated values are removed. getValuesForFieldInPolicyAllTypes(sec, fieldIndex) { const values = []; const ast = this.model.get(sec); if (!ast) { return values; } for (const ptype of ast.keys()) { values.push(...this.getValuesForFieldInPolicy(sec, ptype, fieldIndex)); } return util.arrayRemoveDuplicates(values); } // printPolicy prints the policy to log. printPolicy() { if (!getLogger().isEnable()) { return; } logPrint('Policy:'); this.model.forEach((map, key) => { if (key === 'p' || key === 'g') { map.forEach((ast) => { logPrint(`key, : ${ast.value}, : , ${ast.policy}`); }); } }); } /** * return the field index in fieldMap, if no this field in fieldMap, add it. */ getFieldIndex(ptype, field) { var _a; const assertion = (_a = this.model.get('p')) === null || _a === void 0 ? void 0 : _a.get(ptype); if (!assertion) { return -1; } let index = assertion.fieldIndexMap.get(field); if (index) { return index; } const pattern = ptype + '_' + field; index = -1; for (let i = 0; i < assertion.tokens.length; i++) { if (assertion.tokens[i] === pattern) { index = i; break; } } if (index === -1) { return index; } assertion.fieldIndexMap.set(field, index); return index; } /** * sort policies by subject hieraichy */ sortPoliciesBySubjectHierarchy() { var _a, _b, _c; if (((_b = (_a = this.model.get('e')) === null || _a === void 0 ? void 0 : _a.get('e')) === null || _b === void 0 ? void 0 : _b.value) !== "subjectPriority(p_eft) || deny" /* EffectExpress.SUBJECT_PRIORITY */) { return; } (_c = this.model.get('p')) === null || _c === void 0 ? void 0 : _c.forEach((assertion, ptype) => { const domainIndex = this.getFieldIndex(ptype, "dom" /* FieldIndex.Domain */); const subIndex = this.getFieldIndex(ptype, "sub" /* FieldIndex.Subject */); // eslint-disable-next-line const subjectHierarchyMap = this.getSubjectHierarchyMap(this.model.get('g').get('g').policy); assertion.policy.sort((policyA, policyB) => { const domainA = domainIndex === -1 ? defaultDomain : policyA[domainIndex]; const domainB = domainIndex === -1 ? defaultDomain : policyB[domainIndex]; // eslint-disable-next-line const priorityA = subjectHierarchyMap.get(this.getNameWithDomain(domainA, policyA[subIndex])); // eslint-disable-next-line const priorityB = subjectHierarchyMap.get(this.getNameWithDomain(domainB, policyB[subIndex])); return priorityB - priorityA; }); }); } /** * Calculate the priority of each policy store in Map<string, number> */ getSubjectHierarchyMap(groupPolicies) { const subjectHierarchyMap = new Map(); if (!groupPolicies) { return subjectHierarchyMap; } const policyMap = new Map(); let domain = defaultDomain; groupPolicies.forEach((policy) => { if (policy.length !== 2) domain = policy[this.getFieldIndex('p', "dom" /* FieldIndex.Domain */)]; const child = this.getNameWithDomain(domain, policy[this.getFieldIndex('p', "sub" /* FieldIndex.Subject */)]); const parent = this.getNameWithDomain(domain, policy[this.getFieldIndex('p', "obj" /* FieldIndex.Object */)]); policyMap.set(child, parent); if (!subjectHierarchyMap.has(child)) { subjectHierarchyMap.set(child, 0); } if (!subjectHierarchyMap.has(parent)) { subjectHierarchyMap.set(parent, 0); } subjectHierarchyMap.set(child, 1); }); const set = new Set(); subjectHierarchyMap.forEach((_, key) => { if (subjectHierarchyMap.get(key) !== 0) set.add(key); }); while (set.size !== 0) { for (const child of set.values()) { this.findHierarchy(policyMap, subjectHierarchyMap, set, child); } } return subjectHierarchyMap; } findHierarchy(policyMap, subjectHierarchyMap, set, child) { set.delete(child); // eslint-disable-next-line const parent = policyMap.get(child); if (set.has(parent)) { this.findHierarchy(policyMap, subjectHierarchyMap, set, parent); } // eslint-disable-next-line subjectHierarchyMap.set(child, subjectHierarchyMap.get(parent) + 10); } /** * get full name with domain */ getNameWithDomain(domain, name) { return domain + defaultSeparator + name; } } /** * newModel creates a model. */ export function newModel(...text) { const m = new Model(); if (text.length === 2) { if (text[0] !== '') { m.loadModelFromFile(text[0]); } } else if (text.length === 1) { m.loadModelFromText(text[0]); } else if (text.length !== 0) { throw new Error('Invalid parameters for model.'); } return m; } /** * newModelFromFile creates a model from a .CONF file. */ export function newModelFromFile(path, fs) { const m = new Model(); if (path) { m.loadModelFromFile(path, fs); } return m; } /** * newModelFromString creates a model from a string which contains model text. */ export function newModelFromString(text) { const m = new Model(); m.loadModelFromText(text); return m; }