casbin
Version:
An authorization library that supports access control models like ACL, RBAC, ABAC in Node.JS
558 lines (557 loc) • 19.4 kB
JavaScript
// 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;
}