blob: 9705b6a6a03cdf9be01db5566cd553cea08ee1f0 [file] [log] [blame]
// 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 rbac from '../rbac';
import * as util from '../util';
import { Config, ConfigInterface } from '../config';
import { Assertion } from './assertion';
import { getLogger, logPrint } from '../log';
import { DefaultRoleManager } from '../rbac';
export const sectionNameMap: { [index: string]: string } = {
r: 'request_definition',
p: 'policy_definition',
g: 'role_definition',
e: 'policy_effect',
m: 'matchers',
};
export enum PolicyOp {
PolicyAdd,
PolicyRemove,
}
export const requiredSections = ['r', 'p', 'e', 'm'];
export class Model {
// Model represents the whole access control model.
// Mest-map is the collection of assertions, can be "r", "p", "g", "e", "m".
public model: Map<string, Map<string, Assertion>>;
/**
* constructor is the constructor for Model.
*/
constructor() {
this.model = new Map<string, Map<string, Assertion>>();
}
private loadAssertion(cfg: ConfigInterface, sec: string, key: string): boolean {
const secName = sectionNameMap[sec];
const value = cfg.getString(`${secName}::${key}`);
return this.addDef(sec, key, value);
}
private static getKeySuffix(i: number): string {
if (i === 1) {
return '';
}
return i.toString();
}
private loadSection(cfg: ConfigInterface, sec: string): void {
let i = 1;
for (;;) {
if (!this.loadAssertion(cfg, sec, sec + Model.getKeySuffix(i))) {
break;
} else {
i++;
}
}
}
// addDef adds an assertion to the model.
public addDef(sec: string, key: string, value: string): boolean {
if (value === '') {
return false;
}
const ast = new Assertion();
ast.key = key;
ast.value = value;
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);
});
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<string, Assertion>();
assertionMap.set(key, ast);
this.model.set(sec, assertionMap);
}
return true;
}
// loadModel loads the model from model CONF string.
public loadModel(string: string): void {
const cfg = Config.newConfig(string);
this.loadModelFromConfig(cfg);
}
// loadModelFromText loads the model from the text.
public loadModelFromText(text: string): void {
const cfg = Config.newConfigFromText(text);
this.loadModelFromConfig(cfg);
}
public loadModelFromConfig(cfg: ConfigInterface): void {
for (const s in sectionNameMap) {
this.loadSection(cfg, s);
}
const ms: string[] = [];
requiredSections.forEach((n) => {
if (!this.hasSection(n)) {
ms.push(sectionNameMap[n]);
}
});
if (ms.length > 0) {
throw new Error(`missing required sections: ${ms.join(',')}`);
}
}
private hasSection(sec: string): boolean {
return this.model.get(sec) !== undefined;
}
// printModel prints the model to the log.
public printModel(): void {
logPrint('Model:');
this.model.forEach((value, key) => {
value.forEach((ast, astKey) => {
logPrint(`${key}.${astKey}: ${ast.value}`);
});
});
}
// buildIncrementalRoleLinks provides incremental build the role inheritance relations.
public async buildIncrementalRoleLinks(rm: rbac.RoleManager, op: PolicyOp, sec: string, ptype: string, rules: string[][]): Promise<void> {
if (sec === 'g') {
await this.model.get(sec)?.get(ptype)?.buildIncrementalRoleLinks(rm, op, rules);
}
}
// buildRoleLinks initializes the roles in RBAC.
public async buildRoleLinks(rmMap: Map<string, rbac.RoleManager>): Promise<void> {
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?.buildRoleLinks(rm);
}
}
// clearPolicy clears all current policy.
public clearPolicy(): void {
this.model.forEach((value, key) => {
if (key === 'p' || key === 'g') {
value.forEach((ast) => {
ast.policy = [];
});
}
});
}
// getPolicy gets all rules in a policy.
public getPolicy(sec: string, key: string): string[][] {
const policy: string[][] = [];
const ast = this.model.get(sec)?.get(key);
if (ast) {
policy.push(...ast.policy);
}
return policy;
}
// hasPolicy determines whether a model has the specified policy rule.
public hasPolicy(sec: string, key: string, rule: string[]): boolean {
const ast = this.model.get(sec)?.get(key);
if (!ast) {
return false;
}
return ast.policy.some((n: string[]) => util.arrayEquals(n, rule));
}
// addPolicy adds a policy rule to the model.
public addPolicy(sec: string, key: string, rule: string[]): boolean {
if (!this.hasPolicy(sec, key, rule)) {
const ast = this.model.get(sec)?.get(key);
if (!ast) {
return false;
}
const policy = ast.policy;
const tokens = ast.tokens;
const priorityIndex = tokens.indexOf('p_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.
public addPolicies(sec: string, ptype: string, rules: string[][]): [boolean, string[][]] {
const ast = this.model.get(sec)?.get(ptype);
if (!ast) {
return [false, []];
}
for (const rule of rules) {
if (this.hasPolicy(sec, ptype, rule)) {
return [false, []];
}
}
const priorityFlag = ast.tokens.indexOf('p_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
public updatePolicy(sec: string, ptype: string, oldRule: string[], newRule: string[]): boolean {
const ast = this.model.get(sec)?.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('p_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.
public removePolicy(sec: string, key: string, rule: string[]): boolean {
if (this.hasPolicy(sec, key, rule)) {
const ast = this.model.get(sec)?.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.
public removePolicies(sec: string, ptype: string, rules: string[][]): [boolean, string[][]] {
const effects: string[][] = [];
const ast = this.model.get(sec)?.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: string[]) => {
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.
public getFilteredPolicy(sec: string, key: string, fieldIndex: number, ...fieldValues: string[]): string[][] {
const res: string[][] = [];
const ast = this.model.get(sec)?.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.
public removeFilteredPolicy(sec: string, key: string, fieldIndex: number, ...fieldValues: string[]): [boolean, string[][]] {
const res = [];
const effects: string[][] = [];
let bool = false;
if (fieldValues.length === 0) {
return [false, effects];
}
const ast = this.model.get(sec)?.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.
public getValuesForFieldInPolicy(sec: string, key: string, fieldIndex: number): string[] {
const values: string[] = [];
const ast = this.model.get(sec)?.get(key);
if (!ast) {
return values;
}
return util.arrayRemoveDuplicates(ast.policy.map((n: string[]) => n[fieldIndex]));
}
// getValuesForFieldInPolicyAllTypes gets all values for a field for all rules in a policy of all ptypes, duplicated values are removed.
public getValuesForFieldInPolicyAllTypes(sec: string, fieldIndex: number): string[] {
const values: string[] = [];
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.
public printPolicy(): void {
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}`);
});
}
});
}
}
/**
* newModel creates a model.
*/
export function newModel(...text: string[]): Model {
const m = new Model();
if (text.length === 2) {
if (text[0] !== '') {
m.loadModel(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;
}
/**
* newModelFromString creates a model from a string which contains model text.
*/
export function newModelFromString(text: string): Model {
const m = new Model();
m.loadModelFromText(text);
return m;
}