This is an automated email from the ASF dual-hosted git repository.

EmryZhang pushed a commit to branch prep/node-5.52.0-domain-inference-20261107
in repository https://gitbox.apache.org/repos/asf/casbin-node-casbin.git

commit 2ba8f92ce1480ae53def3e2e22d9a7ac798a36e1
Author: Zhangyanrui123 <[email protected]>
AuthorDate: Sat Nov 7 23:22:07 2026 +0800

    fix: infer domain patterns only from required matcher conditions
---
 src/coreEnforcer.ts               | 64 ++++++++++++++++++++++++----
 test/roleManagerInference.test.ts | 88 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 144 insertions(+), 8 deletions(-)

diff --git a/src/coreEnforcer.ts b/src/coreEnforcer.ts
index 8cfe6f4..927f130 100644
--- a/src/coreEnforcer.ts
+++ b/src/coreEnforcer.ts
@@ -12,7 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { compile, compileAsync, addBinaryOp } from '@casbin/expression-eval';
+import * as expressionEvaluator from '@casbin/expression-eval';
+
+const { compile, compileAsync, addBinaryOp, parse } = ((expressionEvaluator as 
any).default ??
+  expressionEvaluator) as typeof expressionEvaluator;
 
 import { DefaultEffector, Effect, Effector } from './effect';
 import { FunctionMap, Model, newModelFromFile, PolicyOp } from './model';
@@ -216,21 +219,66 @@ export class CoreEnforcer {
         const roleManager = new DefaultRoleManager(10);
         this.rmMap.set(ptype, roleManager);
         const count = (ast.value.match(/_/g) || []).length - 
ast.paramsTokens.length;
-        if (count > 2 && this.usesKeyMatchForDomain()) {
+        if (count === 3 && this.usesKeyMatchForDomain(ptype)) {
           roleManager.addDomainMatchingFunc(keyMatchFunc);
         }
       }
     }
   }
 
-  private usesKeyMatchForDomain(): boolean {
-    const rDomainToken = this.model.model.get('r')?.get('r')?.tokens[1];
-    const pDomainToken = this.model.model.get('p')?.get('p')?.tokens[1];
-    if (!rDomainToken || !pDomainToken) {
+  private usesKeyMatchForDomain(ptype: string): boolean {
+    const requestTokens = this.model.model.get('r')?.get('r')?.tokens ?? [];
+    const policyTokens = this.model.model.get('p')?.get('p')?.tokens ?? [];
+    const matcher = this.model.model.get('m')?.get('m')?.value;
+    if (!matcher) {
+      return false;
+    }
+    // Infer a domain pattern only from positive, required matcher conditions.
+    // Text inside a literal, an optional branch or an unrelated resource match
+    // must not change the role manager's domain isolation.
+    const required = (node: any, predicate: (node: any) => boolean): boolean 
=> {
+      if (predicate(node)) {
+        return true;
+      }
+      return (
+        (node.type === 'LogicalExpression' || node.type === 
'BinaryExpression') &&
+        node.operator === '&&' &&
+        (required(node.left, predicate) || required(node.right, predicate))
+      );
+    };
+    try {
+      addBinaryOp('in', 6, customIn);
+      const expression = parse(matcher);
+      return required(expression, (node) => {
+        if (node.type !== 'CallExpression' || node.callee.type !== 
'Identifier' || node.callee.name !== 'keyMatch') {
+          return false;
+        }
+        const [requestDomain, policyDomain] = node.arguments;
+        if (
+          node.arguments.length !== 2 ||
+          requestDomain?.type !== 'Identifier' ||
+          policyDomain?.type !== 'Identifier' ||
+          !requestTokens.includes(requestDomain.name) ||
+          !policyTokens.includes(policyDomain.name)
+        ) {
+          return false;
+        }
+        return required(expression, (role) => {
+          return (
+            role.type === 'CallExpression' &&
+            role.callee.type === 'Identifier' &&
+            role.callee.name === ptype &&
+            role.arguments.length === 3 &&
+            role.arguments[2].type === 'Identifier' &&
+            role.arguments[2].name === requestDomain.name
+          );
+        });
+      });
+    } catch {
+      // Unsupported expressions keep exact-domain matching. Explicitly
+      // registered domain matching functions remain available to callers.
       return false;
     }
-    const matcher = this.model.model.get('m')?.get('m')?.value ?? '';
-    return matcher.includes(`keyMatch(${rDomainToken}, ${pDomainToken})`);
   }
 
   public sortPolicies(): void {
diff --git a/test/roleManagerInference.test.ts 
b/test/roleManagerInference.test.ts
new file mode 100644
index 0000000..02dd91e
--- /dev/null
+++ b/test/roleManagerInference.test.ts
@@ -0,0 +1,88 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { newEnforcer, newModelFromString, Util } from '../src';
+
+const base = 'g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && 
r.act == p.act';
+
+async function enforcer(matcher: string, policyDomain = 'tenant-b'): 
Promise<any> {
+  const model = newModelFromString(`[request_definition]
+r = sub, dom, obj, act
+[policy_definition]
+p = sub, dom, obj, act
+[role_definition]
+g = _, _, _
+g2 = _, _, _
+[policy_effect]
+e = some(where (p.eft == allow))
+[matchers]
+m = ${matcher}`);
+  const e = await newEnforcer(model);
+  await e.addGroupingPolicy('alice', 'admin', 'tenant*');
+  await e.addPolicy('admin', policyDomain, 'data1', 'read');
+  return e;
+}
+
+test.each([
+  '',
+  ' && "keyMatch(r_dom, p_dom)" == "keyMatch(r_dom, p_dom)"',
+  " && 'keyMatch(r_dom, p_dom)' == 'keyMatch(r_dom, p_dom)'",
+  ' && (true || keyMatch(r.dom, p.dom))',
+  ' && !(false && keyMatch(r.dom, p.dom))',
+])('domain isolation is unchanged by an optional or quoted condition: %s', 
async (extra) => {
+  const e = await enforcer(base + extra);
+  await expect(e.enforce('alice', 'tenant-b', 'data1', 
'read')).resolves.toBe(false);
+});
+
+test.each(['keyMatch(r.dom, p.dom)', 'keyMatch(r.dom,p.dom)', 'keyMatch( r.dom 
, p.dom )'])(
+  'required domain matching preserves pattern-role support: %s',
+  async (match) => {
+    const e = await enforcer(`g(r.sub, p.sub, r.dom) && (${match}) && r.obj == 
p.obj && r.act == p.act`, 'tenant*');
+    await expect(e.enforce('alice', 'tenant-b', 'data1', 
'read')).resolves.toBe(true);
+    await expect(e.enforce('alice', 'other', 'data1', 
'read')).resolves.toBe(false);
+  }
+);
+
+test('a resource pattern is not inferred as a domain pattern', async () => {
+  const model = newModelFromString(`[request_definition]
+r = sub, obj, dom, act
+[policy_definition]
+p = sub, obj, dom, act
+[role_definition]
+g = _, _, _
+[policy_effect]
+e = some(where (p.eft == allow))
+[matchers]
+m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && keyMatch(r.obj, p.obj) && 
r.act == p.act`);
+  const e = await newEnforcer(model);
+  await e.addGroupingPolicy('alice', 'admin', 'tenant*');
+  await e.addPolicy('admin', 'data*', 'tenant-b', 'read');
+  await expect(e.enforce('alice', 'data1', 'tenant-b', 
'read')).resolves.toBe(false);
+});
+
+test('domain inference is scoped to the required role function', async () => {
+  const e = await enforcer('g(r.sub, p.sub, r.dom) && keyMatch(r.dom, p.dom) 
&& r.obj == p.obj && r.act == p.act', 'tenant*');
+  await e.addNamedGroupingPolicy('g2', 'alice', 'admin', 'tenant*');
+  await expect(e.enforce('alice', 'tenant-b', 'data1', 
'read')).resolves.toBe(true);
+  await expect(e.getNamedRoleManager('g2').hasLink('alice', 'admin', 
'tenant-b')).resolves.toBe(false);
+});
+
+test('explicit domain matching remains available for exact-domain models', 
async () => {
+  const e = await enforcer(base);
+  await e.getRoleManager().addDomainMatchingFunc(Util.keyMatchFunc);
+  await expect(e.enforce('alice', 'tenant-b', 'data1', 
'read')).resolves.toBe(true);
+});


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to