From cc04e659a1c3b78bb12dcccbb2149bfd9d96c97c Mon Sep 17 00:00:00 2001 From: Zixuan Liu Date: Sat, 25 Apr 2020 23:26:46 +0800 Subject: [PATCH] feat: add addMatchingFunc to DefaultRoleManager --- examples/rbac_with_pattern_model.conf | 15 ++++++++ examples/rbac_with_pattern_policy.csv | 17 +++++++++ src/coreEnforcer.ts | 7 ++++ src/rbac/defaultRoleManager.ts | 54 ++++++++++++++++++++++++--- test/model.test.ts | 37 +++++++++++++++++- 5 files changed, 123 insertions(+), 7 deletions(-) create mode 100644 examples/rbac_with_pattern_model.conf create mode 100644 examples/rbac_with_pattern_policy.csv diff --git a/examples/rbac_with_pattern_model.conf b/examples/rbac_with_pattern_model.conf new file mode 100644 index 0000000..84580d9 --- /dev/null +++ b/examples/rbac_with_pattern_model.conf @@ -0,0 +1,15 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _ +g2 = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub) && g2(r.obj, p.obj) && regexMatch(r.act, p.act) \ No newline at end of file diff --git a/examples/rbac_with_pattern_policy.csv b/examples/rbac_with_pattern_policy.csv new file mode 100644 index 0000000..a8c06c7 --- /dev/null +++ b/examples/rbac_with_pattern_policy.csv @@ -0,0 +1,17 @@ +p, alice, /pen/1, GET +p, alice, /pen2/1, GET +p, book_admin, book_group, GET +p, pen_admin, pen_group, GET + +g, alice, book_admin +g, bob, pen_admin + +g, /book/*, book_group +g, cathy, /book/1/2/3/4/5 +g, cathy, pen_admin + +g2, /book/:id, book_group +g2, /pen/:id, pen_group + +g2, /book2/{id}, book_group +g2, /pen2/{id}, pen_group \ No newline at end of file diff --git a/src/coreEnforcer.ts b/src/coreEnforcer.ts index 040685d..56e5cfc 100644 --- a/src/coreEnforcer.ts +++ b/src/coreEnforcer.ts @@ -120,6 +120,13 @@ export class CoreEnforcer { this.rm = rm; } + /** + * getRoleManager gets the current role manager. + */ + public getRoleManager(): RoleManager { + return this.rm; + } + /** * setEffector sets the current effector. * diff --git a/src/rbac/defaultRoleManager.ts b/src/rbac/defaultRoleManager.ts index 32ebfb8..13b076e 100644 --- a/src/rbac/defaultRoleManager.ts +++ b/src/rbac/defaultRoleManager.ts @@ -67,10 +67,14 @@ class Role { } } +type MatchingFunc = (arg1: string, arg2: string) => boolean; + // RoleManager provides a default implementation for the RoleManager interface export class DefaultRoleManager implements RoleManager { private allRoles: Map; private maxHierarchyLevel: number; + private hasPattern = false; + private matchingFunc: MatchingFunc; /** * DefaultRoleManager is the constructor for creating an instance of the @@ -83,6 +87,19 @@ export class DefaultRoleManager implements RoleManager { this.maxHierarchyLevel = maxHierarchyLevel; } + /** + * e.buildRoleLinks must be called after addMatchingFunc(). + * @param name + * @param fn + * @example ```javascript + * await e.GetRoleManager().addMatchingFunc('matcher', util.keyMatch); await e.buildRoleLinks(); + * ``` + */ + public async addMatchingFunc(name: string, fn: MatchingFunc): Promise { + this.hasPattern = true; + this.matchingFunc = fn; + } + /** * addLink adds the inheritance link between role: name1 and role: name2. * aka role: name1 inherits role: name2. @@ -210,17 +227,42 @@ export class DefaultRoleManager implements RoleManager { } private createRole(name: string): Role { - const role = this.allRoles.get(name); - if (role) { - return role; - } else { + let role = this.allRoles.get(name); + if (!role) { const newRole = new Role(name); + role = newRole; this.allRoles.set(name, newRole); - return newRole; } + + if (!this.hasPattern) { + return role; + } + + for (const roleName of this.allRoles.keys()) { + if (!(this.matchingFunc(name, roleName) && name !== roleName)) { + continue; + } + + const inherit = this.allRoles.get(roleName); + if (inherit) { + role.addRole(inherit); + } + } + + return role; } private hasRole(name: string): boolean { - return this.allRoles.has(name); + if (!this.hasPattern) { + return this.allRoles.has(name); + } else { + for (const role of this.allRoles.keys()) { + if (this.matchingFunc(name, role)) { + return true; + } + } + } + + return false; } } diff --git a/test/model.test.ts b/test/model.test.ts index 920a201..5defe2c 100644 --- a/test/model.test.ts +++ b/test/model.test.ts @@ -13,7 +13,8 @@ // limitations under the License. import * as _ from 'lodash'; -import { newEnforcer, Enforcer, newModel } from '../src'; +import { DefaultRoleManager, Enforcer, newEnforcer, newModel } from '../src'; +import { keyMatch2Func, keyMatch3Func } from '../src/util'; async function testEnforce(e: Enforcer, sub: string, obj: any, act: string, res: boolean): Promise { await expect(e.enforce(sub, obj, act)).resolves.toBe(res); @@ -300,3 +301,37 @@ test('TestMatcher', async () => { expect(m.model.get('m')?.get('m')?.value).toEqual(`keyMatch(r_obj, ".*get$") || regexMatch(r_act, ".user.")`); }); + +test('TestRBACModelWithPattern', async () => { + const e = await newEnforcer('examples/rbac_with_pattern_model.conf', 'examples/rbac_with_pattern_policy.csv'); + + // Here's a little confusing: the matching function here is not the custom function used in matcher. + // It is the matching function used by "g" (and "g2", "g3" if any..) + // You can see in policy that: "g2, /book/:id, book_group", so in "g2()" function in the matcher, instead + // of checking whether "/book/:id" equals the obj: "/book/1", it checks whether the pattern matches. + // You can see it as normal RBAC: "/book/:id" == "/book/1" becomes KeyMatch2("/book/:id", "/book/1") + const rm = e.getRoleManager() as DefaultRoleManager; + await rm.addMatchingFunc('KeyMatch2', keyMatch2Func); + await e.buildRoleLinks(); + await testEnforce(e, 'alice', '/book/1', 'GET', true); + await testEnforce(e, 'alice', '/book/2', 'GET', true); + await testEnforce(e, 'alice', '/pen/1', 'GET', true); + await testEnforce(e, 'alice', '/pen/2', 'GET', false); + await testEnforce(e, 'bob', '/book/1', 'GET', false); + await testEnforce(e, 'bob', '/book/2', 'GET', false); + await testEnforce(e, 'bob', '/pen/1', 'GET', true); + await testEnforce(e, 'bob', '/pen/2', 'GET', true); + + // AddMatchingFunc() is actually setting a function because only one function is allowed, + // so when we set "KeyMatch3", we are actually replacing "KeyMatch2" with "KeyMatch3". + await rm.addMatchingFunc('KeyMatch3', keyMatch3Func); + await e.buildRoleLinks(); + await testEnforce(e, 'alice', '/book2/1', 'GET', true); + await testEnforce(e, 'alice', '/book2/2', 'GET', true); + await testEnforce(e, 'alice', '/pen2/1', 'GET', true); + await testEnforce(e, 'alice', '/pen2/2', 'GET', false); + await testEnforce(e, 'bob', '/book2/1', 'GET', false); + await testEnforce(e, 'bob', '/book2/2', 'GET', false); + await testEnforce(e, 'bob', '/pen2/1', 'GET', true); + await testEnforce(e, 'bob', '/pen2/2', 'GET', true); +});