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]
