From 7d0c45671a0e162e0e5e35f8af3e738a68c4f278 Mon Sep 17 00:00:00 2001 From: Shreyas Adiyodi Date: Fri, 19 Feb 2021 21:07:01 +0530 Subject: [PATCH] feat: implement ability to load subset of a policy --- lib/casbin/JsonEnforcer.ts | 4 + lib/casbin/JsonFilteredEnforcer.ts | 179 +++++++++++++++++++++++++++++ lib/casbin/index.ts | 54 ++++++--- test/app/group/resource.ts | 1 - test/lib/casbin/sample.ts | 2 +- 5 files changed, 225 insertions(+), 15 deletions(-) create mode 100644 lib/casbin/JsonFilteredEnforcer.ts diff --git a/lib/casbin/JsonEnforcer.ts b/lib/casbin/JsonEnforcer.ts index 9074b3b6a..9db25c098 100644 --- a/lib/casbin/JsonEnforcer.ts +++ b/lib/casbin/JsonEnforcer.ts @@ -49,6 +49,10 @@ export class JsonEnforcer extends CachedEnforcer { await this.invalidateCache(); } + public async addStrPolicy(subject: string, resource: string, action: string) { + await this.addPolicy(subject, resource, action); + } + public async removeJsonPolicy( subject: JsonAttributes, resource: JsonAttributes, diff --git a/lib/casbin/JsonFilteredEnforcer.ts b/lib/casbin/JsonFilteredEnforcer.ts new file mode 100644 index 000000000..d5498d035 --- /dev/null +++ b/lib/casbin/JsonFilteredEnforcer.ts @@ -0,0 +1,179 @@ +/* eslint-disable class-methods-use-this */ +/* eslint-disable max-classes-per-file */ +import { createQueryBuilder, In, Like } from 'typeorm'; +import { newEnforcerWithClass } from 'casbin'; +// eslint-disable-next-line import/no-cycle +import { convertJSONToStringInOrder, JsonEnforcer } from './JsonEnforcer'; + +type JsonAttributes = Record; +type OneKey = Record; + +export class JsonFilteredEnforcer extends JsonEnforcer { + private static params: any[]; + + public static setParams(params: any[]) { + this.params = params; + } + + public static async getEnforcer() { + return newEnforcerWithClass(JsonEnforcer, ...JsonFilteredEnforcer.params); + } + + public async loadPolicySubset(policyObj: any, enforcer: JsonEnforcer) { + const rawSubjects = await createQueryBuilder() + .select('casbin_rule.v1') + .from('casbin_rule', 'casbin_rule') + .where('casbin_rule.ptype = :type', { type: 'g' }) + .andWhere('casbin_rule.v0 like :subject', { + subject: convertJSONToStringInOrder(policyObj.subject) + }) + .getRawMany(); + const subjects = rawSubjects + .map((rG) => rG.v1) + .concat(convertJSONToStringInOrder(policyObj.subject)); + + const anyAction = Like('%*%'); + + await enforcer.loadFilteredPolicy({ + where: [ + { ptype: 'p', v0: In(subjects) }, + { ptype: 'g', v0: convertJSONToStringInOrder(policyObj.subject) }, + { ptype: 'g2', v0: convertJSONToStringInOrder(policyObj.resource) }, + { ptype: 'g3', v0: convertJSONToStringInOrder(policyObj.action) }, + { ptype: 'g3', v0: anyAction } + ] + }); + } + + public async enforceJson( + subject: JsonAttributes, + resource: JsonAttributes, + action: JsonAttributes + ): Promise { + // intantiate new enforcer + + const enforcer = await JsonFilteredEnforcer.getEnforcer(); + + // load filtered policy + await this.loadPolicySubset({ subject, resource, action }, enforcer); + // enforceJson + + const hasAccess = await enforcer.enforce( + convertJSONToStringInOrder(subject), + convertJSONToStringInOrder(resource), + convertJSONToStringInOrder(action) + ); + + return hasAccess; + } + + public async addJsonPolicy( + subject: JsonAttributes, + resource: JsonAttributes, + action: JsonAttributes + ) { + const enforcer = await JsonFilteredEnforcer.getEnforcer(); + await enforcer.addPolicy( + convertJSONToStringInOrder(subject), + convertJSONToStringInOrder(resource), + convertJSONToStringInOrder(action) + ); + } + + public async addStrPolicy(subject: string, resource: string, action: string) { + const enforcer = await JsonFilteredEnforcer.getEnforcer(); + await enforcer.addPolicy(subject, resource, action); + } + + public async removeJsonPolicy( + subject: JsonAttributes, + resource: JsonAttributes, + action: JsonAttributes + ) { + const enforcer = await JsonFilteredEnforcer.getEnforcer(); + await enforcer.removePolicy( + convertJSONToStringInOrder(subject), + convertJSONToStringInOrder(resource), + convertJSONToStringInOrder(action) + ); + } + + public async addSubjectGroupingJsonPolicy( + subject: OneKey, + jsonAttributes: JsonAttributes + ) { + const enforcer = await JsonFilteredEnforcer.getEnforcer(); + await enforcer.addNamedGroupingPolicy( + 'g', + convertJSONToStringInOrder(subject), + convertJSONToStringInOrder(jsonAttributes), + 'subject' + ); + } + + public async removeSubjectGroupingJsonPolicy( + subject: OneKey, + jsonAttributes: JsonAttributes + ) { + const enforcer = await JsonFilteredEnforcer.getEnforcer(); + await enforcer.removeNamedGroupingPolicy( + 'g', + convertJSONToStringInOrder(subject), + convertJSONToStringInOrder(jsonAttributes), + 'subject' + ); + } + + public async addResourceGroupingJsonPolicy( + resource: OneKey, + jsonAttributes: JsonAttributes + ) { + const enforcer = await JsonFilteredEnforcer.getEnforcer(); + await enforcer.addNamedGroupingPolicy( + 'g2', + convertJSONToStringInOrder(resource), + convertJSONToStringInOrder(jsonAttributes), + 'resource' + ); + } + + // ? Note: this will remove all policies by resource keys and then insert the new one + public async upsertResourceGroupingJsonPolicy( + resource: OneKey, + jsonAttributes: JsonAttributes + ) { + await this.removeAllResourceGroupingJsonPolicy(resource); + await this.addResourceGroupingJsonPolicy(resource, jsonAttributes); + } + + public async removeAllResourceGroupingJsonPolicy( + resource: OneKey + ) { + const enforcer = await JsonFilteredEnforcer.getEnforcer(); + await enforcer.removeFilteredNamedGroupingPolicy( + 'g2', + 0, + convertJSONToStringInOrder(resource) + ); + } + + public async addActionGroupingJsonPolicy( + action: OneKey, + jsonAttributes: JsonAttributes + ) { + const enforcer = await JsonFilteredEnforcer.getEnforcer(); + await enforcer.addNamedGroupingPolicy( + 'g3', + convertJSONToStringInOrder(action), + convertJSONToStringInOrder(jsonAttributes), + 'action' + ); + } +} + +export async function newJsonFilteredEnforcer( + ...params: any[] +): Promise { + JsonFilteredEnforcer.setParams(params); + return newEnforcerWithClass(JsonFilteredEnforcer, ...params); +} diff --git a/lib/casbin/index.ts b/lib/casbin/index.ts index 12ede703a..1c6d624cc 100644 --- a/lib/casbin/index.ts +++ b/lib/casbin/index.ts @@ -2,7 +2,11 @@ import TypeORMAdapter from 'typeorm-adapter'; import * as CasbinPgWatcher from 'casbin-pg-watcher'; import { join } from 'path'; -import { newJsonEnforcer, JsonEnforcer } from './JsonEnforcer'; +import { JsonEnforcer, newJsonEnforcer } from './JsonEnforcer'; +import { + newJsonFilteredEnforcer, + JsonFilteredEnforcer +} from './JsonFilteredEnforcer'; const { newWatcher } = CasbinPgWatcher; @@ -10,7 +14,37 @@ class CasbinSingleton { // eslint-disable-next-line no-useless-constructor private constructor() {} - public static enforcer: null | JsonEnforcer = null; + public static enforcer: null | JsonFilteredEnforcer | JsonEnforcer = null; + + public static filtered = true; + + private static async initJsonEnforcer( + modelPath: any, + policyAdapter: any, + dbConnectionUrl: string + ) { + const policyWatcher = await newWatcher({ + connectionString: dbConnectionUrl + }); + + this.enforcer = await newJsonEnforcer(modelPath, policyAdapter); + + this.enforcer.setWatcher(policyWatcher); + this.enforcer.enableAutoSave(true); + this.enforcer.enableLog(false); + + // Load the policy from DB. + await this.enforcer.loadPolicy(); + } + + private static async initJsonFilteredEnforcer( + modelPath: any, + policyAdapter: any + ) { + this.enforcer = await newJsonFilteredEnforcer(modelPath, policyAdapter); + this.enforcer.enableAutoSave(true); + this.enforcer.enableLog(false); + } public static async create(dbConnectionUrl: string) { if (!this.enforcer) { @@ -18,19 +52,13 @@ class CasbinSingleton { type: 'postgres', url: dbConnectionUrl }); - - const policyWatcher = await newWatcher({ - connectionString: dbConnectionUrl - }); const modelPath = join(__dirname, 'model.conf'); - this.enforcer = await newJsonEnforcer(modelPath, policyAdapter); - this.enforcer.setWatcher(policyWatcher); - this.enforcer.enableAutoSave(true); - this.enforcer.enableLog(false); - - // Load the policy from DB. - await this.enforcer.loadPolicy(); + if (CasbinSingleton.filtered) { + await this.initJsonFilteredEnforcer(modelPath, policyAdapter); + } else { + await this.initJsonEnforcer(modelPath, policyAdapter, dbConnectionUrl); + } } return this.enforcer; diff --git a/test/app/group/resource.ts b/test/app/group/resource.ts index cef2cd5d8..2bbce8f75 100644 --- a/test/app/group/resource.ts +++ b/test/app/group/resource.ts @@ -407,7 +407,6 @@ lab.experiment('Group::resource', () => { userPolicies: [] } ]; - debugger; const sortedResult = parseResults(result); const sortedExpectedResult = parseResults(expectedResult); Code.expect(sortedResult).to.equal(sortedExpectedResult); diff --git a/test/lib/casbin/sample.ts b/test/lib/casbin/sample.ts index 43c1a5407..77380b1c5 100644 --- a/test/lib/casbin/sample.ts +++ b/test/lib/casbin/sample.ts @@ -98,7 +98,7 @@ const setupPolicies = async () => { { role: 'resource.viewer' } ); - await CasbinSingleton.enforcer.addPolicy( + await CasbinSingleton.enforcer.addStrPolicy( JSON.stringify({ team: 'de' }), '*', JSON.stringify({ role: 'super.admin' })