This is an automated email from the ASF dual-hosted git repository. EmryZhang pushed a commit to branch chore/release-readiness-20261009 in repository https://gitbox.apache.org/repos/asf/casbin-node-casbin.git
commit 43b78abf45e2f68748c59fd8c11307dfe0b9666a Author: Zhangyanrui123 <[email protected]> AuthorDate: Sun Nov 1 15:35:27 2026 +0800 chore: finalize apache-casbin 5.52.0 release preparation --- .github/workflows/main.yml | 5 ++- .github/workflows/release.yml | 12 +++++- NOTICE | 1 + RELEASING.md | 11 +++++ src/coreEnforcer.ts | 64 ++++++++++++++++++++++++---- test/conformance.test.ts | 25 ++++++----- test/roleManagerInference.test.ts | 88 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 185 insertions(+), 21 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6ff4199..96df10a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -34,18 +34,21 @@ jobs: - uses: actions/checkout@v4 with: repository: apache/casbin + ref: 8f5173b17bee2675025e7b46c9a8116ee352ec84 path: casbin-conformance sparse-checkout: conformance - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - - run: yarn install + - run: yarn install --frozen-lockfile --ignore-scripts - run: yarn lint - run: yarn test env: CASBIN_CONFORMANCE_DIR: ${{ github.workspace }}/casbin-conformance/conformance - run: yarn coverage + env: + CASBIN_CONFORMANCE_DIR: ${{ github.workspace }}/casbin-conformance/conformance - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9d6858a..747ee07 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -96,14 +96,24 @@ jobs: with: node-version: '22' registry-url: 'https://registry.npmjs.org' + - name: Check out pinned conformance fixtures + if: steps.meta.outputs.is_rc == 'false' + uses: actions/checkout@v4 + with: + repository: apache/casbin + ref: 8f5173b17bee2675025e7b46c9a8116ee352ec84 + path: casbin-conformance + sparse-checkout: conformance + persist-credentials: false - name: Build and test npm package from approved source if: steps.meta.outputs.is_rc == 'false' env: NAME: ${{ steps.meta.outputs.name }} + CASBIN_CONFORMANCE_DIR: ${{ github.workspace }}/casbin-conformance/conformance run: | set -euo pipefail cd "dist-release/source/$NAME" - yarn install --frozen-lockfile + yarn install --frozen-lockfile --ignore-scripts yarn lint yarn test --runInBand yarn build diff --git a/NOTICE b/NOTICE index fa0a801..697b252 100644 --- a/NOTICE +++ b/NOTICE @@ -13,3 +13,4 @@ Copyright 2020 The Casbin Authors. All Rights Reserved. Copyright 2021 The Casbin Authors. All Rights Reserved. Copyright 2022 The Casbin Authors. All Rights Reserved. Copyright 2023 The Casbin Authors. All Rights Reserved. +Copyright 2026 The Casbin Authors. All Rights Reserved. diff --git a/RELEASING.md b/RELEASING.md index b07d8b6..ca3ade0 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -30,3 +30,14 @@ Every distribution includes LICENSE, NOTICE and DISCLAIMER. Existing GitHub asse ## Registry identity migration The adopted registry name is `apache-casbin`. Historical package versions remain untouched. Confirm administration of this exact new name before publishing; old-package credentials are not sufficient evidence. Preserve existing version lineage in the engineering candidate; project review adopts the final version. Do not publish placeholders, RCs or snapshots to reserve a name. + +## Tests and conformance fixtures + +Use a frozen Yarn install and run lint, tests and both module builds. To include +the shared cross-language cases, check out `apache/casbin` at +`8f5173b17bee2675025e7b46c9a8116ee352ec84` outside the source tree and set +`CASBIN_CONFORMANCE_DIR` to its `conformance` directory before running the tests. +CI and the final publication workflow use this same pinned fixture revision. +These fixtures are test inputs and are not bundled in the source or npm package. +Without the variable the optional conformance test is skipped; report that +coverage explicitly when reviewing a release. diff --git a/src/coreEnforcer.ts b/src/coreEnforcer.ts index 571e112..539d0fd 100644 --- a/src/coreEnforcer.ts +++ b/src/coreEnforcer.ts @@ -15,7 +15,10 @@ // 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'; @@ -219,21 +222,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/conformance.test.ts b/test/conformance.test.ts index e5b19a3..5364f3a 100644 --- a/test/conformance.test.ts +++ b/test/conformance.test.ts @@ -1,16 +1,19 @@ -// Copyright 2026 The Casbin Authors. All Rights Reserved. +// 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 // -// Licensed 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 // -// http://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. +// 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 { readdirSync, readFileSync } from 'fs'; import * as path from 'path'; 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]
