blob: df17e45017b5e8541ef3ec0014b278c09b68d166 [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 { compile, compileAsync, addBinaryOp } from 'expression-eval';
import { DefaultEffector, Effect, Effector } from './effect';
import { FunctionMap, Model, newModel, PolicyOp } from './model';
import { Adapter, FilteredAdapter, Watcher } from './persist';
import { DefaultRoleManager, RoleManager } from './rbac';
import { EnforceContext } from './enforceContext';
import {
escapeAssertion,
generateGFunction,
getEvalValue,
hasEval,
replaceEval,
generatorRunSync,
generatorRunAsync,
customIn,
bracketCompatible,
} from './util';
import { getLogger, logPrint } from './log';
import { MatchingFunc } from './rbac';
type Matcher = ((context: any) => Promise<any>) | ((context: any) => any);
type EnforceResult = Generator<(boolean | [boolean, string[]]) | Promise<boolean | [boolean, string[]]>>;
/**
* CoreEnforcer defines the core functionality of an enforcer.
*/
export class CoreEnforcer {
protected modelPath: string;
protected model: Model;
protected fm: FunctionMap = FunctionMap.loadFunctionMap();
protected eft: Effector = new DefaultEffector();
private matcherMap: Map<string, Matcher> = new Map();
private defaultEnforceContext: EnforceContext = new EnforceContext('r', 'p', 'e', 'm');
protected adapter: Adapter;
protected watcher: Watcher | null = null;
protected rmMap: Map<string, RoleManager>;
protected enabled = true;
protected autoSave = true;
protected autoBuildRoleLinks = true;
protected autoNotifyWatcher = true;
private getExpression(asyncCompile: boolean, exp: string): Matcher {
const matcherKey = `${asyncCompile ? 'ASYNC[' : 'SYNC['}${exp}]`;
addBinaryOp('in', 1, customIn);
let expression = this.matcherMap.get(matcherKey);
if (!expression) {
exp = bracketCompatible(exp);
expression = asyncCompile ? compileAsync(exp) : compile(exp);
this.matcherMap.set(matcherKey, expression);
}
return expression;
}
/**
* loadModel reloads the model from the model CONF string.
* Because the policy is attached to a model,
* so the policy is invalidated and needs to be reloaded by calling LoadPolicy().
*/
public loadModel(): void {
this.model = newModel();
this.model.loadModel(this.modelPath);
this.model.printModel();
}
/**
* getModel gets the current model.
*
* @return the model of the enforcer.
*/
public getModel(): Model {
return this.model;
}
/**
* setModel sets the current model.
*
* @param m the model.
*/
public setModel(m: Model): void {
this.model = m;
}
/**
* getAdapter gets the current adapter.
*
* @return the adapter of the enforcer.
*/
public getAdapter(): Adapter {
return this.adapter;
}
/**
* setAdapter sets the current adapter.
*
* @param adapter the adapter.
*/
public setAdapter(adapter: Adapter): void {
this.adapter = adapter;
}
/**
* setWatcher sets the current watcher.
*
* @param watcher the watcher.
*/
public setWatcher(watcher: Watcher): void {
this.watcher = watcher;
watcher.setUpdateCallback(async () => await this.loadPolicy());
}
/**
* setRoleManager sets the current role manager.
*
* @param rm the role manager.
*/
public setRoleManager(rm: RoleManager): void {
this.rmMap.set('g', rm);
}
/**
* getRoleManager gets the current role manager.
*/
public getRoleManager(): RoleManager {
return <RoleManager>this.rmMap.get('g');
}
/**
* getNamedRoleManager gets role manager by name.
*/
public getNamedRoleManager(name: string): RoleManager | undefined {
return this.rmMap.get(name);
}
/**
* setEffector sets the current effector.
*
* @param eft the effector.
*/
public setEffector(eft: Effector): void {
this.eft = eft;
}
/**
* clearPolicy clears all policy.
*/
public clearPolicy(): void {
this.model.clearPolicy();
}
public initRmMap(): void {
this.rmMap = new Map<string, RoleManager>();
const rm = this.model.model.get('g');
if (rm) {
for (const ptype of rm.keys()) {
this.rmMap.set(ptype, new DefaultRoleManager(10));
}
}
}
public sortPolicies(): void {
const policy = this.model.model.get('p')?.get('p')?.policy;
const tokens = this.model.model.get('p')?.get('p')?.tokens;
if (policy && tokens) {
const priorityIndex = tokens.indexOf('p_priority');
if (priorityIndex !== -1) {
policy.sort((a, b) => {
return parseInt(a[priorityIndex], 10) - parseInt(b[priorityIndex], 10);
});
}
}
}
/**
* loadPolicy reloads the policy from adapter.
*/
public async loadPolicy(): Promise<void> {
this.model.clearPolicy();
await this.adapter.loadPolicy(this.model);
this.sortPolicies();
if (this.autoBuildRoleLinks) {
await this.buildRoleLinksInternal();
}
}
/**
* loadFilteredPolicy reloads a filtered policy from adapter.
*
* @param filter the filter used to specify which type of policy should be loaded.
*/
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
public async loadFilteredPolicy(filter: any): Promise<boolean> {
this.model.clearPolicy();
return this.loadIncrementalFilteredPolicy(filter);
}
/**
* LoadIncrementalFilteredPolicy append a filtered policy from adapter.
*
* @param filter the filter used to specify which type of policy should be appended.
*/
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
public async loadIncrementalFilteredPolicy(filter: any): Promise<boolean> {
if ('isFiltered' in this.adapter) {
await (this.adapter as FilteredAdapter).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.
*/
public isFiltered(): boolean {
if ('isFiltered' in this.adapter) {
return (this.adapter as FilteredAdapter).isFiltered();
}
return false;
}
/**
* savePolicy saves the current policy (usually after changed with
* Casbin API) back to adapter.
*/
public async savePolicy(): Promise<boolean> {
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.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.
*/
public enableEnforce(enable: boolean): void {
this.enabled = enable;
}
/**
* enableLog changes whether to print Casbin log to the standard output.
*
* @param enable whether to enable Casbin's log.
*/
public enableLog(enable: boolean): void {
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.
*/
public enableAutoSave(autoSave: boolean): void {
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.
*/
public enableAutoNotifyWatcher(enable: boolean): void {
this.autoNotifyWatcher = 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.
*/
public enableAutoBuildRoleLinks(autoBuildRoleLinks: boolean): void {
this.autoBuildRoleLinks = autoBuildRoleLinks;
}
/**
* add matching function to RoleManager by ptype
* @param ptype g
* @param fn the function will be added
*/
public async addNamedMatchingFunc(ptype: string, fn: MatchingFunc): Promise<void> {
const rm = this.rmMap.get(ptype);
if (rm) {
return await (<DefaultRoleManager>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
*/
public async addNamedDomainMatchingFunc(ptype: string, fn: MatchingFunc): Promise<void> {
const rm = this.rmMap.get(ptype);
if (rm) {
return await (<DefaultRoleManager>rm).addDomainMatchingFunc(fn);
}
}
/**
* buildRoleLinks manually rebuild the role inheritance relations.
*/
public async buildRoleLinks(): Promise<void> {
return this.buildRoleLinksInternal();
}
/**
* buildIncrementalRoleLinks provides incremental build the role inheritance relations.
* @param op policy operation
* @param ptype g
* @param rules policies
*/
public async buildIncrementalRoleLinks(op: PolicyOp, ptype: string, rules: string[][]): Promise<void> {
let rm = this.rmMap.get(ptype);
if (!rm) {
rm = new DefaultRoleManager(10);
this.rmMap.set(ptype, rm);
}
await this.model.buildIncrementalRoleLinks(rm, op, 'g', ptype, rules);
}
protected async buildRoleLinksInternal(): Promise<void> {
for (const rm of this.rmMap.values()) {
await rm.clear();
await this.model.buildRoleLinks(this.rmMap);
}
}
private *privateEnforce(
asyncCompile = true,
explain = false,
enforceContext: EnforceContext = new EnforceContext('r', 'p', 'e', 'm'),
...rvals: any[]
): EnforceResult {
if (!this.enabled) {
return true;
}
let explainIndex = -1;
const functions: { [key: string]: any } = {};
this.fm.getFunctions().forEach((value: any, key: string) => {
functions[key] = value;
});
const astMap = this.model.model.get('g');
astMap?.forEach((value, key) => {
const rm = value.rm;
functions[key] = generateGFunction(rm);
});
const expString = this.model.model.get('m')?.get(enforceContext.mType)?.value;
if (!expString) {
throw new Error('Unable to find matchers in model');
}
const effectExpr = this.model.model.get('e')?.get(enforceContext.eType)?.value;
if (!effectExpr) {
throw new Error('Unable to find policy_effect in model');
}
const HasEval: boolean = hasEval(expString);
let expression: Matcher | undefined = undefined;
const p = this.model.model.get('p')?.get(enforceContext.pType);
const policyLen = p?.policy?.length;
const rTokens = this.model.model.get('r')?.get(enforceContext.rType)?.tokens;
const rTokensLen = rTokens?.length;
const effectStream = this.eft.newStream(effectExpr);
if (policyLen && policyLen !== 0) {
for (let i = 0; i < policyLen; i++) {
const parameters: { [key: string]: any } = {};
if (rTokens?.length !== rvals.length) {
throw new Error(`invalid request size: expected ${rTokensLen}, got ${rvals.length}, rvals: ${rvals}"`);
}
rTokens.forEach((token, j) => {
parameters[token] = rvals[j];
});
p?.tokens.forEach((token, j) => {
parameters[token] = p?.policy[i][j];
});
if (HasEval) {
const ruleNames: string[] = getEvalValue(expString);
let expWithRule = expString;
for (const ruleName of ruleNames) {
if (ruleName in parameters) {
const rule = escapeAssertion(parameters[ruleName]);
expWithRule = 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 = { ...parameters, ...functions };
const result = asyncCompile ? yield expression(context) : expression(context);
let eftRes: Effect;
switch (typeof result) {
case 'boolean':
eftRes = result ? Effect.Allow : Effect.Indeterminate;
break;
case 'number':
if (result === 0) {
eftRes = Effect.Indeterminate;
} else {
eftRes = result;
}
break;
case 'string':
if (result === '') {
eftRes = Effect.Indeterminate;
} else {
eftRes = Effect.Allow;
}
break;
default:
throw new Error('matcher result should only be of type boolean, number, or string');
}
const eft = parameters['p_eft'];
if (eft && eftRes === Effect.Allow) {
if (eft === 'allow') {
eftRes = Effect.Allow;
} else if (eft === 'deny') {
eftRes = Effect.Deny;
} else {
eftRes = Effect.Indeterminate;
}
}
const [res, done] = effectStream.pushEffect(eftRes);
if (done) {
explainIndex = i;
break;
}
}
} else {
explainIndex = 0;
const parameters: { [key: string]: any } = {};
rTokens?.forEach((token, j): void => {
parameters[token] = rvals[j];
});
p?.tokens?.forEach((token) => {
parameters[token] = '';
});
expression = this.getExpression(asyncCompile, expString);
const context = { ...parameters, ...functions };
const result = asyncCompile ? yield expression(context) : expression(context);
if (result) {
effectStream.pushEffect(Effect.Allow);
} else {
effectStream.pushEffect(Effect.Indeterminate);
}
}
const res = effectStream.current();
// only generate the request --> result string if the message
// is going to be logged.
if (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}`;
logPrint(reqStr);
}
if (explain) {
if (explainIndex === -1) {
return [res, []];
}
return [res, p?.policy[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.
*/
public enforceSync(...rvals: any[]): boolean {
if (rvals[0] instanceof EnforceContext) {
const enforceContext: EnforceContext = rvals.shift();
return generatorRunSync(this.privateEnforce(false, false, enforceContext, ...rvals));
}
return 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.
*/
public enforceExSync(...rvals: any[]): [boolean, string[]] {
if (rvals[0] instanceof EnforceContext) {
const enforceContext: EnforceContext = rvals.shift();
return generatorRunSync(this.privateEnforce(false, true, enforceContext, ...rvals));
}
return generatorRunSync(this.privateEnforce(false, true, this.defaultEnforceContext, ...rvals));
}
/**
* Same as enforceSync. To be removed.
*/
public enforceWithSyncCompile(...rvals: any[]): boolean {
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.
*/
public async enforce(...rvals: any[]): Promise<boolean> {
if (rvals[0] instanceof EnforceContext) {
const enforceContext: EnforceContext = rvals.shift();
return generatorRunAsync(this.privateEnforce(true, false, enforceContext, ...rvals));
}
return generatorRunAsync(this.privateEnforce(true, false, 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.
*/
public async enforceEx(...rvals: any[]): Promise<[boolean, string[]]> {
if (rvals[0] instanceof EnforceContext) {
const enforceContext: EnforceContext = rvals.shift();
return generatorRunAsync(this.privateEnforce(true, true, enforceContext, ...rvals));
}
return generatorRunAsync(this.privateEnforce(true, true, 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.
*/
public async batchEnforce(rvals: any[]): Promise<boolean[]> {
return await Promise.all(rvals.map((rval) => this.enforce(...rval)));
}
}