Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 56 additions & 8 deletions src/coreEnforcer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 {
Expand Down
88 changes: 88 additions & 0 deletions test/roleManagerInference.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
Loading