Skip to content

Commit cc04e65

Browse files
committed
feat: add addMatchingFunc to DefaultRoleManager
1 parent bf0b66e commit cc04e65

File tree

5 files changed

+123
-7
lines changed

5 files changed

+123
-7
lines changed

examples/rbac_with_pattern_model.conf

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[request_definition]
2+
r = sub, obj, act
3+
4+
[policy_definition]
5+
p = sub, obj, act
6+
7+
[role_definition]
8+
g = _, _
9+
g2 = _, _
10+
11+
[policy_effect]
12+
e = some(where (p.eft == allow))
13+
14+
[matchers]
15+
m = g(r.sub, p.sub) && g2(r.obj, p.obj) && regexMatch(r.act, p.act)

examples/rbac_with_pattern_policy.csv

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
p, alice, /pen/1, GET
2+
p, alice, /pen2/1, GET
3+
p, book_admin, book_group, GET
4+
p, pen_admin, pen_group, GET
5+
6+
g, alice, book_admin
7+
g, bob, pen_admin
8+
9+
g, /book/*, book_group
10+
g, cathy, /book/1/2/3/4/5
11+
g, cathy, pen_admin
12+
13+
g2, /book/:id, book_group
14+
g2, /pen/:id, pen_group
15+
16+
g2, /book2/{id}, book_group
17+
g2, /pen2/{id}, pen_group

src/coreEnforcer.ts

+7
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,13 @@ export class CoreEnforcer {
120120
this.rm = rm;
121121
}
122122

123+
/**
124+
* getRoleManager gets the current role manager.
125+
*/
126+
public getRoleManager(): RoleManager {
127+
return this.rm;
128+
}
129+
123130
/**
124131
* setEffector sets the current effector.
125132
*

src/rbac/defaultRoleManager.ts

+48-6
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,14 @@ class Role {
6767
}
6868
}
6969

70+
type MatchingFunc = (arg1: string, arg2: string) => boolean;
71+
7072
// RoleManager provides a default implementation for the RoleManager interface
7173
export class DefaultRoleManager implements RoleManager {
7274
private allRoles: Map<string, Role>;
7375
private maxHierarchyLevel: number;
76+
private hasPattern = false;
77+
private matchingFunc: MatchingFunc;
7478

7579
/**
7680
* DefaultRoleManager is the constructor for creating an instance of the
@@ -83,6 +87,19 @@ export class DefaultRoleManager implements RoleManager {
8387
this.maxHierarchyLevel = maxHierarchyLevel;
8488
}
8589

90+
/**
91+
* e.buildRoleLinks must be called after addMatchingFunc().
92+
* @param name
93+
* @param fn
94+
* @example ```javascript
95+
* await e.GetRoleManager().addMatchingFunc('matcher', util.keyMatch); await e.buildRoleLinks();
96+
* ```
97+
*/
98+
public async addMatchingFunc(name: string, fn: MatchingFunc): Promise<void> {
99+
this.hasPattern = true;
100+
this.matchingFunc = fn;
101+
}
102+
86103
/**
87104
* addLink adds the inheritance link between role: name1 and role: name2.
88105
* aka role: name1 inherits role: name2.
@@ -210,17 +227,42 @@ export class DefaultRoleManager implements RoleManager {
210227
}
211228

212229
private createRole(name: string): Role {
213-
const role = this.allRoles.get(name);
214-
if (role) {
215-
return role;
216-
} else {
230+
let role = this.allRoles.get(name);
231+
if (!role) {
217232
const newRole = new Role(name);
233+
role = newRole;
218234
this.allRoles.set(name, newRole);
219-
return newRole;
220235
}
236+
237+
if (!this.hasPattern) {
238+
return role;
239+
}
240+
241+
for (const roleName of this.allRoles.keys()) {
242+
if (!(this.matchingFunc(name, roleName) && name !== roleName)) {
243+
continue;
244+
}
245+
246+
const inherit = this.allRoles.get(roleName);
247+
if (inherit) {
248+
role.addRole(inherit);
249+
}
250+
}
251+
252+
return role;
221253
}
222254

223255
private hasRole(name: string): boolean {
224-
return this.allRoles.has(name);
256+
if (!this.hasPattern) {
257+
return this.allRoles.has(name);
258+
} else {
259+
for (const role of this.allRoles.keys()) {
260+
if (this.matchingFunc(name, role)) {
261+
return true;
262+
}
263+
}
264+
}
265+
266+
return false;
225267
}
226268
}

test/model.test.ts

+36-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
// limitations under the License.
1414

1515
import * as _ from 'lodash';
16-
import { newEnforcer, Enforcer, newModel } from '../src';
16+
import { DefaultRoleManager, Enforcer, newEnforcer, newModel } from '../src';
17+
import { keyMatch2Func, keyMatch3Func } from '../src/util';
1718

1819
async function testEnforce(e: Enforcer, sub: string, obj: any, act: string, res: boolean): Promise<void> {
1920
await expect(e.enforce(sub, obj, act)).resolves.toBe(res);
@@ -300,3 +301,37 @@ test('TestMatcher', async () => {
300301

301302
expect(m.model.get('m')?.get('m')?.value).toEqual(`keyMatch(r_obj, ".*get$") || regexMatch(r_act, ".user.")`);
302303
});
304+
305+
test('TestRBACModelWithPattern', async () => {
306+
const e = await newEnforcer('examples/rbac_with_pattern_model.conf', 'examples/rbac_with_pattern_policy.csv');
307+
308+
// Here's a little confusing: the matching function here is not the custom function used in matcher.
309+
// It is the matching function used by "g" (and "g2", "g3" if any..)
310+
// You can see in policy that: "g2, /book/:id, book_group", so in "g2()" function in the matcher, instead
311+
// of checking whether "/book/:id" equals the obj: "/book/1", it checks whether the pattern matches.
312+
// You can see it as normal RBAC: "/book/:id" == "/book/1" becomes KeyMatch2("/book/:id", "/book/1")
313+
const rm = e.getRoleManager() as DefaultRoleManager;
314+
await rm.addMatchingFunc('KeyMatch2', keyMatch2Func);
315+
await e.buildRoleLinks();
316+
await testEnforce(e, 'alice', '/book/1', 'GET', true);
317+
await testEnforce(e, 'alice', '/book/2', 'GET', true);
318+
await testEnforce(e, 'alice', '/pen/1', 'GET', true);
319+
await testEnforce(e, 'alice', '/pen/2', 'GET', false);
320+
await testEnforce(e, 'bob', '/book/1', 'GET', false);
321+
await testEnforce(e, 'bob', '/book/2', 'GET', false);
322+
await testEnforce(e, 'bob', '/pen/1', 'GET', true);
323+
await testEnforce(e, 'bob', '/pen/2', 'GET', true);
324+
325+
// AddMatchingFunc() is actually setting a function because only one function is allowed,
326+
// so when we set "KeyMatch3", we are actually replacing "KeyMatch2" with "KeyMatch3".
327+
await rm.addMatchingFunc('KeyMatch3', keyMatch3Func);
328+
await e.buildRoleLinks();
329+
await testEnforce(e, 'alice', '/book2/1', 'GET', true);
330+
await testEnforce(e, 'alice', '/book2/2', 'GET', true);
331+
await testEnforce(e, 'alice', '/pen2/1', 'GET', true);
332+
await testEnforce(e, 'alice', '/pen2/2', 'GET', false);
333+
await testEnforce(e, 'bob', '/book2/1', 'GET', false);
334+
await testEnforce(e, 'bob', '/book2/2', 'GET', false);
335+
await testEnforce(e, 'bob', '/pen2/1', 'GET', true);
336+
await testEnforce(e, 'bob', '/pen2/2', 'GET', true);
337+
});

0 commit comments

Comments
 (0)