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]

Reply via email to