This is an automated email from the ASF dual-hosted git repository.
hsluoyz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/casbin-node-casbin.git
The following commit(s) were added to refs/heads/master by this push:
new 80648e0 fix: match wildcard domains in getImplicitPermissionsForUser
80648e0 is described below
commit 80648e03a3522fe12568231e1243511316d7eeb7
Author: Yang Luo <[email protected]>
AuthorDate: Fri Aug 7 00:16:59 2026 +0800
fix: match wildcard domains in getImplicitPermissionsForUser
---
.../rbac_with_domain_pattern_in_policy_model.conf | 14 +++
.../rbac_with_domain_pattern_in_policy_policy.csv | 16 +++
src/enforcer.ts | 131 ++++++++++++++++++---
src/rbac/defaultRoleManager.ts | 63 ++++++++++
src/rbac/roleManager.ts | 10 ++
test/rbac/defaultRoleManager.test.ts | 44 +++++++
test/rbacAPI.test.ts | 76 ++++++++++++
7 files changed, 336 insertions(+), 18 deletions(-)
diff --git a/examples/rbac_with_domain_pattern_in_policy_model.conf
b/examples/rbac_with_domain_pattern_in_policy_model.conf
new file mode 100644
index 0000000..cc8e252
--- /dev/null
+++ b/examples/rbac_with_domain_pattern_in_policy_model.conf
@@ -0,0 +1,14 @@
+[request_definition]
+r = sub, obj, act, dom
+
+[policy_definition]
+p = sub, obj, act, dom, eft
+
+[role_definition]
+g = _, _, _
+
+[policy_effect]
+e = some(where (p.eft == allow)) && !some(where (p.eft == deny))
+
+[matchers]
+m = g(r.sub, p.sub, r.dom) && (p.dom == "*" || r.dom == p.dom) && r.obj ==
p.obj && r.act == p.act
diff --git a/examples/rbac_with_domain_pattern_in_policy_policy.csv
b/examples/rbac_with_domain_pattern_in_policy_policy.csv
new file mode 100644
index 0000000..b06a2e0
--- /dev/null
+++ b/examples/rbac_with_domain_pattern_in_policy_policy.csv
@@ -0,0 +1,16 @@
+p, abstract_roles1, devis, read, *, allow
+p, abstract_roles1, devis, create, *, allow
+p, abstract_roles2, devis, read, *, allow
+p, abstract_roles2, organization, read, *, allow
+p, abstract_roles2, organization, write, *, allow
+p, roles1, devis, delete, tenant1, allow
+
+g, roles1, abstract_roles1, tenant1
+g, roles1, abstract_roles1, tenant2
+g, roles2, abstract_roles2, tenant1
+g, roles2, abstract_roles2, tenant2
+g, super_user, abstract_roles2, *
+
+g, michael, roles1, tenant1
+g, thomas, roles2, tenant2
+g, theo, super_user, *
diff --git a/src/enforcer.ts b/src/enforcer.ts
index ff589bf..57c20f8 100644
--- a/src/enforcer.ts
+++ b/src/enforcer.ts
@@ -356,22 +356,55 @@ export class Enforcer extends ManagementEnforcer {
* But getImplicitRolesForUser("alice") will get: ["role:admin",
"role:user"].
*/
public async getImplicitRolesForUser(name: string, ...domain: string[]):
Promise<string[]> {
- const res = new Set<string>();
+ const res: string[] = [];
+
+ // Each role definition is a hierarchy of its own, so every role manager
is walked
+ // separately and the results are concatenated. Following a "g" link and
then a "g2"
+ // link off the role it led to would mix two unrelated hierarchies.
+ for (const ptype of this.rmMap.keys()) {
+ res.push(...(await this.getNamedImplicitRolesForUser(ptype, name,
...domain)));
+ }
+
+ return res;
+ }
+
+ /**
+ * getNamedImplicitRolesForUser gets implicit roles that a user has, using
only the
+ * given role definition. Compared to getImplicitRolesForUser(), which walks
every
+ * role manager, this one is restricted to "g", "g2", ...
+ *
+ * @param ptype the role definition type, can be "g", "g2", "g3", ..
+ * @param name the user.
+ * @param domain the domain, optional.
+ */
+ public async getNamedImplicitRolesForUser(ptype: string, name: string,
...domain: string[]): Promise<string[]> {
+ const rm = this.rmMap.get(ptype);
+ if (!rm) {
+ throw new Error(`role manager ${ptype} is not initialized`);
+ }
+
+ if (rm.getImplicitRoles) {
+ return rm.getImplicitRoles(name, ...domain);
+ }
+
+ // Fallback for role managers that only expose one hop. It cannot honour a
hierarchy
+ // level limit, since that limit belongs to the role manager.
+ const res: string[] = [];
+ const visited = new Set<string>([name]);
const q = [name];
let n: string | undefined;
while ((n = q.shift()) !== undefined) {
- for (const rm of this.rmMap.values()) {
- const role = await rm.getRoles(n, ...domain);
- role.forEach((r) => {
- if (!res.has(r)) {
- res.add(r);
- q.push(r);
- }
- });
- }
+ const roles = await rm.getRoles(n, ...domain);
+ roles.forEach((r) => {
+ if (!visited.has(r)) {
+ visited.add(r);
+ res.push(r);
+ q.push(r);
+ }
+ });
}
- return Array.from(res);
+ return res;
}
/**
@@ -386,16 +419,78 @@ export class Enforcer extends ManagementEnforcer {
* But getImplicitPermissionsForUser("alice") will get: [["admin", "data1",
"read"], ["alice", "data2", "read"]].
*/
public async getImplicitPermissionsForUser(user: string, ...domain:
string[]): Promise<string[][]> {
- const roles = await this.getImplicitRolesForUser(user, ...domain);
- roles.unshift(user);
- const res: string[][] = [];
+ return this.getNamedImplicitPermissionsForUser('p', 'g', user, ...domain);
+ }
- for (const n of roles) {
- const p = await this.getPermissionsForUser(n, ...domain);
- res.push(...p);
+ /**
+ * getNamedImplicitPermissionsForUser gets implicit permissions for a user
or role
+ * by the named policy and the named role definition.
+ *
+ * When a domain is given, a policy rule is kept if its domain field matches
that
+ * domain according to the role manager, so a rule written for a wildcard
domain
+ * (e.g. "p, admin, data, read, *") is reported for every concrete domain
once a
+ * domain matching function has been registered with
addNamedDomainMatchingFunc().
+ * The returned rule then carries the requested domain instead of the
pattern.
+ *
+ * @param ptype the policy type, can be "p", "p2", "p3", ..
+ * @param gtype the role definition type, can be "g", "g2", "g3", ..
+ * @param user the user.
+ * @param domain the domain, optional.
+ */
+ public async getNamedImplicitPermissionsForUser(ptype: string, gtype:
string, user: string, ...domain: string[]): Promise<string[][]> {
+ if (domain.length > 1) {
+ throw new Error('error: domain should be 1 parameter');
}
- return res;
+ const rm = this.rmMap.get(gtype);
+ if (!rm) {
+ throw new Error(`role manager ${gtype} is not initialized`);
+ }
+
+ const roles = await this.getNamedImplicitRolesForUser(gtype, user,
...domain);
+ const policyRoles = new Set<string>(roles);
+ policyRoles.add(user);
+
+ // The subject and the domain are not necessarily the first two tokens, so
both
+ // have to be looked up in the model instead of being assumed to sit at a
fixed index.
+ const subIndex = this.getFieldIndex(ptype, FieldIndex.Subject);
+ if (subIndex === -1) {
+ throw new Error(`${FieldIndex.Subject} index is not set, please use
enforcer.setFieldIndex() to set index`);
+ }
+
+ const permission: string[][] = [];
+ const policy = await this.getNamedPolicy(ptype);
+
+ if (domain.length === 0) {
+ for (const rule of policy) {
+ if (policyRoles.has(rule[subIndex])) {
+ permission.push([...rule]);
+ }
+ }
+ return permission;
+ }
+
+ const domIndex = this.getFieldIndex(ptype, FieldIndex.Domain);
+ if (domIndex === -1) {
+ throw new Error(`${FieldIndex.Domain} index is not set, please use
enforcer.setFieldIndex() to set index`);
+ }
+
+ const d = domain[0];
+ for (const rule of policy) {
+ // match() falls back to an exact comparison unless a domain matching
function
+ // has been registered, so a "*" rule only spreads across domains on
request.
+ const matched = rm.match ? rm.match(d, rule[domIndex]) : d ===
rule[domIndex];
+ if (!matched) {
+ continue;
+ }
+ if (policyRoles.has(rule[subIndex])) {
+ const newRule = [...rule];
+ newRule[domIndex] = d;
+ permission.push(newRule);
+ }
+ }
+
+ return permission;
}
/**
diff --git a/src/rbac/defaultRoleManager.ts b/src/rbac/defaultRoleManager.ts
index 5271f83..78b5eaf 100644
--- a/src/rbac/defaultRoleManager.ts
+++ b/src/rbac/defaultRoleManager.ts
@@ -181,6 +181,24 @@ export class DefaultRoleManager implements RoleManager {
this.domainMatchingFunc = fn;
}
+ /**
+ * match determines whether the domain str is covered by the domain pattern
used
+ * in a policy or grouping rule. Without a domain matching function
registered,
+ * only an exact match counts, which keeps the default behaviour unchanged.
+ *
+ * @param str the concrete domain, e.g. "tenant1"
+ * @param pattern the domain written in the rule, e.g. "*"
+ */
+ public match(str: string, pattern: string): boolean {
+ if (str === pattern) {
+ return true;
+ }
+ if (this.hasDomainPattern) {
+ return this.domainMatchingFunc(str, pattern);
+ }
+ return false;
+ }
+
/**
* addDomainHierarchy sets a rolemanager to define role inheritance between
domains
* @param rm RoleManager to define domain hierarchy
@@ -319,6 +337,51 @@ export class DefaultRoleManager implements RoleManager {
return allRoles.createRole(name, this.matchingFunc).getRoles();
}
+ /**
+ * getImplicitRoles gets the roles that a subject inherits, directly or
through other
+ * roles. Compared to getRoles(), which only walks one hop, this follows the
whole
+ * hierarchy, up to maxHierarchyLevel hops.
+ * domain is a prefix to the roles.
+ */
+ public async getImplicitRoles(name: string, ...domain: string[]):
Promise<string[]> {
+ if (domain.length === 0) {
+ domain = [DEFAULT_DOMAIN];
+ } else if (domain.length > 1) {
+ throw new Error('error: domain should be 1 parameter');
+ }
+
+ // The role graph only has to be resolved once for the whole traversal.
+ const allRoles = this.generateTempRoles(domain[0]);
+
+ const res: string[] = [];
+ // Seeded with the subject so that a cycle back to it does not report the
subject as
+ // one of its own roles, and so that the traversal terminates.
+ const roleSet = new Set<string>([name]);
+ let current = [name];
+
+ for (let level = 0; level < this.maxHierarchyLevel && current.length > 0;
level++) {
+ const next: string[] = [];
+ for (const n of current) {
+ if (!allRoles.hasRole(n, this.matchingFunc)) {
+ continue;
+ }
+ allRoles
+ .createRole(n, this.matchingFunc)
+ .getRoles()
+ .forEach((r) => {
+ if (!roleSet.has(r)) {
+ roleSet.add(r);
+ res.push(r);
+ next.push(r);
+ }
+ });
+ }
+ current = next;
+ }
+
+ return res;
+ }
+
/**
* getUsers gets the users that inherits a subject.
* domain is an unreferenced parameter here, may be used in other
implementations.
diff --git a/src/rbac/roleManager.ts b/src/rbac/roleManager.ts
index 4c3afb7..5b0ffb1 100644
--- a/src/rbac/roleManager.ts
+++ b/src/rbac/roleManager.ts
@@ -31,11 +31,21 @@ export interface RoleManager {
// GetRoles gets the roles that a user inherits.
// domain is a prefix to the roles (can be used for other purposes).
getRoles(name: string, ...domain: string[]): Promise<string[]>;
+ // GetImplicitRoles gets the roles that a user inherits, directly or through
other roles,
+ // respecting the hierarchy level limit of the implementation.
+ // domain is a prefix to the roles (can be used for other purposes).
+ // Optional: when not implemented, callers fall back to walking getRoles()
themselves,
+ // which cannot honour a hierarchy level limit.
+ getImplicitRoles?(name: string, ...domain: string[]): Promise<string[]>;
// GetUsers gets the users that inherits a role.
// domain is a prefix to the users (can be used for other purposes).
getUsers(name: string, ...domain: string[]): Promise<string[]>;
// PrintRoles prints all the roles to log.
printRoles(): Promise<void>;
+ // Match determines whether the domain str is covered by the domain pattern
+ // written in a policy or grouping rule, e.g. "tenant1" against "*".
+ // Optional: when not implemented, callers fall back to an exact comparison.
+ match?(str: string, pattern: string): boolean;
// GetDomains gets domains that a user has
getDomains(name: string): Promise<string[]>;
// GetAllDomains gets all domains
diff --git a/test/rbac/defaultRoleManager.test.ts
b/test/rbac/defaultRoleManager.test.ts
index f35b669..2671df8 100644
--- a/test/rbac/defaultRoleManager.test.ts
+++ b/test/rbac/defaultRoleManager.test.ts
@@ -12,3 +12,47 @@ test('TestAllMatchingFunc', async () => {
// *:/book/:id
expect(await rm.hasLink('/book/1', 'book_group', 'domain1')).toBe(true);
});
+
+test('TestGetImplicitRoles', async () => {
+ const rm = new DefaultRoleManager(10);
+ await rm.addLink('alice', 'admin');
+ await rm.addLink('admin', 'data1_admin');
+ await rm.addLink('admin', 'data2_admin');
+
+ expect(await rm.getRoles('alice')).toEqual(['admin']);
+ expect(await rm.getImplicitRoles('alice')).toEqual(['admin', 'data1_admin',
'data2_admin']);
+ expect(await rm.getImplicitRoles('bob')).toEqual([]);
+
+ // A cycle terminates and never reports the subject as one of its own roles.
+ await rm.addLink('data2_admin', 'alice');
+ expect(await rm.getImplicitRoles('alice')).toEqual(['admin', 'data1_admin',
'data2_admin']);
+});
+
+test('TestGetImplicitRolesRespectsMaxHierarchyLevel', async () => {
+ // role0 -> role1 -> ... -> role5
+ const chain = async (rm: DefaultRoleManager): Promise<void> => {
+ for (let i = 0; i < 5; i++) {
+ await rm.addLink(`role${i}`, `role${i + 1}`);
+ }
+ };
+
+ const rm = new DefaultRoleManager(10);
+ await chain(rm);
+ expect(await rm.getImplicitRoles('role0')).toEqual(['role1', 'role2',
'role3', 'role4', 'role5']);
+
+ // Only maxHierarchyLevel hops are followed, so the tail of the chain is cut
off.
+ const shallow = new DefaultRoleManager(2);
+ await chain(shallow);
+ expect(await shallow.getImplicitRoles('role0')).toEqual(['role1', 'role2']);
+});
+
+test('TestGetImplicitRolesWithDomain', async () => {
+ const rm = new DefaultRoleManager(10);
+ await rm.addLink('alice', 'admin', 'domain1');
+ await rm.addLink('admin', 'data1_admin', 'domain1');
+ await rm.addLink('alice', 'guest', 'domain2');
+
+ expect(await rm.getImplicitRoles('alice', 'domain1')).toEqual(['admin',
'data1_admin']);
+ expect(await rm.getImplicitRoles('alice', 'domain2')).toEqual(['guest']);
+ expect(await rm.getImplicitRoles('alice', 'domain3')).toEqual([]);
+});
diff --git a/test/rbacAPI.test.ts b/test/rbacAPI.test.ts
index 51082b9..69c7146 100644
--- a/test/rbacAPI.test.ts
+++ b/test/rbacAPI.test.ts
@@ -13,6 +13,7 @@
// limitations under the License.
import { newEnforcer } from '../src';
+import { keyMatchFunc } from '../src/util';
test('test getRolesForUser', async () => {
const e = await newEnforcer('examples/rbac_model.conf',
'examples/rbac_with_hierarchy_policy.csv');
@@ -75,6 +76,81 @@ test('test getImplicitPermissionsForUser', async () => {
]);
});
+test('test getImplicitRolesForUser walks each role definition separately',
async () => {
+ const e = await newEnforcer('examples/rbac_with_multiple_policy_model.conf',
'examples/rbac_with_multiple_policy_policy.csv');
+
+ // g: alice -> admin -> user, g2: alice -> user
+ expect(await e.getNamedImplicitRolesForUser('g', 'alice')).toEqual(['admin',
'user']);
+ expect(await e.getNamedImplicitRolesForUser('g2',
'alice')).toEqual(['user']);
+
+ // Each hierarchy is walked on its own and the results are concatenated, so
"user" is
+ // reported once per role definition that grants it.
+ expect(await e.getImplicitRolesForUser('alice')).toEqual(['admin', 'user',
'user']);
+
+ // Following g and then g2 would wrongly hand admin the roles that only
alice has via g2.
+ expect(await e.getImplicitRolesForUser('admin')).toEqual(['user']);
+});
+
+test('test getImplicitRolesForUser with a cycle back to the user', async () =>
{
+ const e = await newEnforcer('examples/rbac_model.conf',
'examples/rbac_with_hierarchy_policy.csv');
+ expect(await e.addGroupingPolicy('data2_admin', 'alice')).toEqual(true);
+
+ // The user is never reported as one of its own roles.
+ expect(await e.getImplicitRolesForUser('alice')).toEqual(['admin',
'data1_admin', 'data2_admin']);
+});
+
+test('test getImplicitPermissionsForUser with a wildcard domain in the
policy', async () => {
+ const e = await newEnforcer(
+ 'examples/rbac_with_domain_pattern_in_policy_model.conf',
+ 'examples/rbac_with_domain_pattern_in_policy_policy.csv'
+ );
+ await e.addNamedDomainMatchingFunc('g', keyMatchFunc);
+ await e.buildRoleLinks();
+
+ // The abstract role is reachable in tenant1 only, so nothing leaks into
tenant2.
+ expect(await e.getImplicitRolesForUser('michael',
'tenant1')).toEqual(['roles1', 'abstract_roles1']);
+ expect(await e.getImplicitRolesForUser('michael', 'tenant2')).toEqual([]);
+
+ // Rules stored on the "*" domain are reported for the concrete domain that
was asked for.
+ expect(await e.getImplicitPermissionsForUser('michael', 'tenant1')).toEqual([
+ ['abstract_roles1', 'devis', 'read', 'tenant1', 'allow'],
+ ['abstract_roles1', 'devis', 'create', 'tenant1', 'allow'],
+ ['roles1', 'devis', 'delete', 'tenant1', 'allow'],
+ ]);
+ expect(await e.getImplicitPermissionsForUser('michael',
'tenant2')).toEqual([]);
+
+ // A rule bound to tenant1 is not reported for tenant2.
+ expect(await e.getImplicitPermissionsForUser('thomas', 'tenant2')).toEqual([
+ ['abstract_roles2', 'devis', 'read', 'tenant2', 'allow'],
+ ['abstract_roles2', 'organization', 'read', 'tenant2', 'allow'],
+ ['abstract_roles2', 'organization', 'write', 'tenant2', 'allow'],
+ ]);
+
+ // A role granted on the "*" domain applies to every domain.
+ expect(await e.getImplicitPermissionsForUser('theo', 'tenant1')).toEqual([
+ ['abstract_roles2', 'devis', 'read', 'tenant1', 'allow'],
+ ['abstract_roles2', 'organization', 'read', 'tenant1', 'allow'],
+ ['abstract_roles2', 'organization', 'write', 'tenant1', 'allow'],
+ ]);
+
+ // getImplicitPermissionsForUser() now agrees with enforce().
+ expect(await e.enforce('michael', 'devis', 'read', 'tenant1')).toEqual(true);
+ expect(await e.enforce('michael', 'devis', 'read',
'tenant2')).toEqual(false);
+ expect(await e.enforce('theo', 'organization', 'write',
'tenant3')).toEqual(true);
+});
+
+test('test getNamedImplicitPermissionsForUser', async () => {
+ const e = await newEnforcer('examples/rbac_model.conf',
'examples/rbac_with_hierarchy_policy.csv');
+ expect(await e.getNamedImplicitPermissionsForUser('p', 'g',
'alice')).toEqual([
+ ['alice', 'data1', 'read'],
+ ['data1_admin', 'data1', 'read'],
+ ['data1_admin', 'data1', 'write'],
+ ['data2_admin', 'data2', 'read'],
+ ['data2_admin', 'data2', 'write'],
+ ]);
+ expect(await e.getNamedImplicitRolesForUser('g', 'alice')).toEqual(['admin',
'data1_admin', 'data2_admin']);
+});
+
test('test getImplicitResourcesForUser', async () => {
const e = await newEnforcer('examples/rbac_with_pattern_model.conf',
'examples/rbac_with_pattern_policy.csv');
expect(await e.getImplicitResourcesForUser('alice')).toEqual([
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]