Skip to content

Commit 70dc873

Browse files
Copilothsluoyz
andcommitted
Add comprehensive enforceEx documentation and examples for RBAC
Co-authored-by: hsluoyz <[email protected]>
1 parent 72b04e9 commit 70dc873

File tree

4 files changed

+164
-4
lines changed

4 files changed

+164
-4
lines changed

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,27 @@ if (res) {
9797
}
9898
```
9999

100+
### Explain enforcement with `enforceEx`
101+
102+
Besides `enforce()`, `node-casbin` also provides `enforceEx()`. This function will return which policy rule was matched when making an enforcement decision. This is especially useful when using RBAC with roles and permissions.
103+
104+
```node.js
105+
// enforceEx returns [boolean, string[]]
106+
// The second element is the matched policy rule
107+
const [allowed, explanation] = await enforcer.enforceEx(sub, obj, act);
108+
109+
if (allowed) {
110+
// permit alice to read data1
111+
console.log('Matched rule:', explanation);
112+
// For direct permissions: ['alice', 'data1', 'read']
113+
// For RBAC permissions: ['role_name', 'data1', 'read'] where alice has role_name
114+
} else {
115+
// deny the request
116+
}
117+
```
118+
119+
**Note**: When using RBAC models, `enforceEx()` will return the matched policy rule which may be associated with a role rather than the user directly. For example, if user `alice` has role `admin` and the policy allows `admin` to `read` `data1`, then `enforceEx('alice', 'data1', 'read')` will return `[true, ['admin', 'data1', 'read']]`.
120+
100121
Besides the static policy file, `node-casbin` also provides API for permission management at run-time.
101122
For example, You can get all the roles assigned to a user as below:
102123

examples/rbac_with_enforce_ex.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright 2024 The Casbin Authors. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
/**
16+
* This example demonstrates how to use enforceEx() with RBAC model.
17+
* enforceEx() returns both the enforcement result and the matched policy rule,
18+
* which is helpful for understanding which permission allowed the request.
19+
*/
20+
21+
const { newEnforcer } = require('casbin');
22+
23+
async function main() {
24+
// Initialize enforcer with RBAC model
25+
const enforcer = await newEnforcer(
26+
'examples/rbac_model.conf',
27+
'examples/rbac_policy.csv'
28+
);
29+
30+
console.log('=== RBAC Model with enforceEx() Demo ===\n');
31+
32+
// Display the policies
33+
console.log('Policies (p):');
34+
const policies = await enforcer.getPolicy();
35+
policies.forEach(p => console.log(` ${p.join(', ')}`));
36+
37+
console.log('\nRole assignments (g):');
38+
const roles = await enforcer.getGroupingPolicy();
39+
roles.forEach(r => console.log(` ${r.join(', ')}`));
40+
41+
console.log('\n=== Enforcement Examples ===\n');
42+
43+
// Example 1: Direct permission
44+
console.log('1. Direct permission check:');
45+
console.log(' Checking: alice, data1, read');
46+
let [allowed, explanation] = await enforcer.enforceEx('alice', 'data1', 'read');
47+
console.log(` Result: ${allowed}`);
48+
console.log(` Matched rule: [${explanation.join(', ')}]`);
49+
console.log(' → Alice has direct permission to read data1\n');
50+
51+
// Example 2: Role-based permission (alice has role data2_admin)
52+
console.log('2. Role-based permission check:');
53+
console.log(' Checking: alice, data2, read');
54+
[allowed, explanation] = await enforcer.enforceEx('alice', 'data2', 'read');
55+
console.log(` Result: ${allowed}`);
56+
console.log(` Matched rule: [${explanation.join(', ')}]`);
57+
console.log(' → Alice has data2_admin role, which can read data2\n');
58+
59+
// Example 3: Role-based permission (alice has role data2_admin)
60+
console.log('3. Role-based permission check:');
61+
console.log(' Checking: alice, data2, write');
62+
[allowed, explanation] = await enforcer.enforceEx('alice', 'data2', 'write');
63+
console.log(` Result: ${allowed}`);
64+
console.log(` Matched rule: [${explanation.join(', ')}]`);
65+
console.log(' → Alice has data2_admin role, which can write data2\n');
66+
67+
// Example 4: No permission
68+
console.log('4. No permission check:');
69+
console.log(' Checking: alice, data2, delete');
70+
[allowed, explanation] = await enforcer.enforceEx('alice', 'data2', 'delete');
71+
console.log(` Result: ${allowed}`);
72+
console.log(` Matched rule: [${explanation.join(', ')}]`);
73+
console.log(' → Alice does not have permission to delete data2\n');
74+
75+
// Example 5: Check what roles a user has
76+
console.log('5. Getting user roles:');
77+
const aliceRoles = await enforcer.getRolesForUser('alice');
78+
console.log(` Alice's roles: [${aliceRoles.join(', ')}]`);
79+
80+
// Example 6: Check permissions for a role
81+
console.log('\n6. Getting role permissions:');
82+
const adminPerms = await enforcer.getPermissionsForUser('data2_admin');
83+
console.log(' data2_admin permissions:');
84+
adminPerms.forEach(p => console.log(` [${p.join(', ')}]`));
85+
86+
console.log('\n=== Key Takeaways ===');
87+
console.log('• enforceEx() returns [boolean, string[]] - result and matched rule');
88+
console.log('• For RBAC, the matched rule shows which policy allowed the access');
89+
console.log('• If access is through a role, the rule contains the role name, not the user');
90+
console.log('• This is useful for debugging and audit trails');
91+
}
92+
93+
main().catch(console.error);

src/coreEnforcer.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -643,12 +643,17 @@ export class CoreEnforcer {
643643
/**
644644
* If the matchers does not contain an asynchronous method, call it faster.
645645
*
646-
* enforceSync decides whether a "subject" can access a "object" with
646+
* enforceExSync decides whether a "subject" can access a "object" with
647647
* the operation "action", input parameters are usually: (sub, obj, act).
648648
*
649+
* This is the synchronous version of enforceEx. It returns both the enforcement
650+
* result and the matched policy rule.
651+
*
649652
* @param rvals the request needs to be mediated, usually an array
650653
* of strings, can be class instances if ABAC is used.
651-
* @return whether to allow the request and the reason rule.
654+
* @return A tuple containing:
655+
* - boolean: whether to allow the request
656+
* - string[]: the matched policy rule (see enforceEx for details)
652657
*/
653658
public enforceExSync(...rvals: any[]): [boolean, string[]] {
654659
if (rvals[0] instanceof EnforceContext) {
@@ -700,12 +705,31 @@ export class CoreEnforcer {
700705
}
701706

702707
/**
703-
* enforce decides whether a "subject" can access a "object" with
708+
* enforceEx decides whether a "subject" can access a "object" with
704709
* the operation "action", input parameters are usually: (sub, obj, act).
705710
*
711+
* This function returns both the enforcement result and the matched policy rule.
712+
* The matched rule is especially useful for RBAC models where access may be granted
713+
* through role inheritance.
714+
*
706715
* @param rvals the request needs to be mediated, usually an array
707716
* of strings, can be class instances if ABAC is used.
708-
* @return whether to allow the request and the reason rule.
717+
* @return A tuple containing:
718+
* - boolean: whether to allow the request
719+
* - string[]: the matched policy rule that allowed/denied the request
720+
* For RBAC models, this will be the role's policy rule if access
721+
* was granted through role inheritance, e.g., ['role_name', 'obj', 'act']
722+
* Returns empty array [] if no policy matched.
723+
*
724+
* @example
725+
* // Direct permission
726+
* const [allowed1, rule1] = await enforcer.enforceEx('alice', 'data1', 'read');
727+
* // allowed1 = true, rule1 = ['alice', 'data1', 'read']
728+
*
729+
* @example
730+
* // RBAC permission (alice has role 'admin', and admin can read data1)
731+
* const [allowed2, rule2] = await enforcer.enforceEx('alice', 'data1', 'read');
732+
* // allowed2 = true, rule2 = ['admin', 'data1', 'read']
709733
*/
710734
public async enforceEx(...rvals: any[]): Promise<[boolean, string[]]> {
711735
if (rvals[0] instanceof EnforceContext) {

test/enforcer.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,28 @@ test('TestKeyGet2', async () => {
696696
expect(await e.enforce('alice', '/data/1')).toBe(true);
697697
});
698698

699+
test('TestEnforceExWithRBACModel', async () => {
700+
// This test demonstrates enforceEx behavior with standard RBAC model
701+
// It shows that enforceEx correctly returns the matched policy rule,
702+
// which may be a role's policy when using role-based access
703+
const e = await newEnforcer('examples/rbac_model.conf', 'examples/rbac_policy.csv');
704+
705+
// Test direct permission - should match alice's direct policy
706+
await testEnforceEx(e, 'alice', 'data1', 'read', [true, ['alice', 'data1', 'read']]);
707+
708+
// Test role-based permission - alice has role data2_admin
709+
// The matched rule should be the role's policy, not alice's
710+
await testEnforceEx(e, 'alice', 'data2', 'read', [true, ['data2_admin', 'data2', 'read']]);
711+
await testEnforceEx(e, 'alice', 'data2', 'write', [true, ['data2_admin', 'data2', 'write']]);
712+
713+
// Test bob's direct permission
714+
await testEnforceEx(e, 'bob', 'data2', 'write', [true, ['bob', 'data2', 'write']]);
715+
716+
// Test failed permission - no matching policy
717+
await testEnforceEx(e, 'bob', 'data2', 'read', [false, []]);
718+
await testEnforceEx(e, 'alice', 'data1', 'write', [false, []]);
719+
});
720+
699721
test('TestEnforceExWithRBACDenyModel', async () => {
700722
const e = await newEnforcer('examples/rbac_with_deny_model.conf', 'examples/rbac_with_deny_policy.csv');
701723
testEnforceEx(e, 'alice', 'data1', 'read', [true, ['alice', 'data1', 'read', 'allow']]);

0 commit comments

Comments
 (0)