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
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,27 @@ if (res) {
}
```

### Explain enforcement with `enforceEx`

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.

```node.js
// enforceEx returns [boolean, string[]]
// The second element is the matched policy rule
const [allowed, explanation] = await enforcer.enforceEx(sub, obj, act);

if (allowed) {
// permit alice to read data1
console.log('Matched rule:', explanation);
// For direct permissions: ['alice', 'data1', 'read']
// For RBAC permissions: ['role_name', 'data1', 'read'] where alice has role_name
} else {
// deny the request
}
```

**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']]`.

Besides the static policy file, `node-casbin` also provides API for permission management at run-time.
For example, You can get all the roles assigned to a user as below:

Expand Down
93 changes: 93 additions & 0 deletions examples/rbac_with_enforce_ex.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright 2024 The Casbin Authors. All Rights Reserved.
//
// 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
//
// 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.

/**
* This example demonstrates how to use enforceEx() with RBAC model.
* enforceEx() returns both the enforcement result and the matched policy rule,
* which is helpful for understanding which permission allowed the request.
*/

const { newEnforcer } = require('casbin');

async function main() {
// Initialize enforcer with RBAC model
const enforcer = await newEnforcer(
'examples/rbac_model.conf',
'examples/rbac_policy.csv'
);

console.log('=== RBAC Model with enforceEx() Demo ===\n');

// Display the policies
console.log('Policies (p):');
const policies = await enforcer.getPolicy();
policies.forEach(p => console.log(` ${p.join(', ')}`));

console.log('\nRole assignments (g):');
const roles = await enforcer.getGroupingPolicy();
roles.forEach(r => console.log(` ${r.join(', ')}`));

console.log('\n=== Enforcement Examples ===\n');

// Example 1: Direct permission
console.log('1. Direct permission check:');
console.log(' Checking: alice, data1, read');
let [allowed, explanation] = await enforcer.enforceEx('alice', 'data1', 'read');
console.log(` Result: ${allowed}`);
console.log(` Matched rule: [${explanation.join(', ')}]`);
console.log(' → Alice has direct permission to read data1\n');

// Example 2: Role-based permission (alice has role data2_admin)
console.log('2. Role-based permission check:');
console.log(' Checking: alice, data2, read');
[allowed, explanation] = await enforcer.enforceEx('alice', 'data2', 'read');
console.log(` Result: ${allowed}`);
console.log(` Matched rule: [${explanation.join(', ')}]`);
console.log(' → Alice has data2_admin role, which can read data2\n');

// Example 3: Role-based permission (alice has role data2_admin)
console.log('3. Role-based permission check:');
console.log(' Checking: alice, data2, write');
[allowed, explanation] = await enforcer.enforceEx('alice', 'data2', 'write');
console.log(` Result: ${allowed}`);
console.log(` Matched rule: [${explanation.join(', ')}]`);
console.log(' → Alice has data2_admin role, which can write data2\n');

// Example 4: No permission
console.log('4. No permission check:');
console.log(' Checking: alice, data2, delete');
[allowed, explanation] = await enforcer.enforceEx('alice', 'data2', 'delete');
console.log(` Result: ${allowed}`);
console.log(` Matched rule: [${explanation.join(', ')}]`);
console.log(' → Alice does not have permission to delete data2\n');

// Example 5: Check what roles a user has
console.log('5. Getting user roles:');
const aliceRoles = await enforcer.getRolesForUser('alice');
console.log(` Alice's roles: [${aliceRoles.join(', ')}]`);

// Example 6: Check permissions for a role
console.log('\n6. Getting role permissions:');
const adminPerms = await enforcer.getPermissionsForUser('data2_admin');
console.log(' data2_admin permissions:');
adminPerms.forEach(p => console.log(` [${p.join(', ')}]`));

console.log('\n=== Key Takeaways ===');
console.log('• enforceEx() returns [boolean, string[]] - result and matched rule');
console.log('• For RBAC, the matched rule shows which policy allowed the access');
console.log('• If access is through a role, the rule contains the role name, not the user');
console.log('• This is useful for debugging and audit trails');
}

main().catch(console.error);
32 changes: 28 additions & 4 deletions src/coreEnforcer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -643,12 +643,17 @@ export class CoreEnforcer {
/**
* If the matchers does not contain an asynchronous method, call it faster.
*
* enforceSync decides whether a "subject" can access a "object" with
* enforceExSync decides whether a "subject" can access a "object" with
* the operation "action", input parameters are usually: (sub, obj, act).
*
* This is the synchronous version of enforceEx. It returns both the enforcement
* result and the matched policy rule.
*
* @param rvals the request needs to be mediated, usually an array
* of strings, can be class instances if ABAC is used.
* @return whether to allow the request and the reason rule.
* @return A tuple containing:
* - boolean: whether to allow the request
* - string[]: the matched policy rule (see enforceEx for details)
*/
public enforceExSync(...rvals: any[]): [boolean, string[]] {
if (rvals[0] instanceof EnforceContext) {
Expand Down Expand Up @@ -700,12 +705,31 @@ export class CoreEnforcer {
}

/**
* enforce decides whether a "subject" can access a "object" with
* enforceEx decides whether a "subject" can access a "object" with
* the operation "action", input parameters are usually: (sub, obj, act).
*
* This function returns both the enforcement result and the matched policy rule.
* The matched rule is especially useful for RBAC models where access may be granted
* through role inheritance.
*
* @param rvals the request needs to be mediated, usually an array
* of strings, can be class instances if ABAC is used.
* @return whether to allow the request and the reason rule.
* @return A tuple containing:
* - boolean: whether to allow the request
* - string[]: the matched policy rule that allowed/denied the request
* For RBAC models, this will be the role's policy rule if access
* was granted through role inheritance, e.g., ['role_name', 'obj', 'act']
* Returns empty array [] if no policy matched.
*
* @example
* // Direct permission
* const [allowed1, rule1] = await enforcer.enforceEx('alice', 'data1', 'read');
* // allowed1 = true, rule1 = ['alice', 'data1', 'read']
*
* @example
* // RBAC permission (alice has role 'admin', and admin can read data1)
* const [allowed2, rule2] = await enforcer.enforceEx('alice', 'data1', 'read');
* // allowed2 = true, rule2 = ['admin', 'data1', 'read']
*/
public async enforceEx(...rvals: any[]): Promise<[boolean, string[]]> {
if (rvals[0] instanceof EnforceContext) {
Expand Down
22 changes: 22 additions & 0 deletions test/enforcer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,28 @@ test('TestKeyGet2', async () => {
expect(await e.enforce('alice', '/data/1')).toBe(true);
});

test('TestEnforceExWithRBACModel', async () => {
// This test demonstrates enforceEx behavior with standard RBAC model
// It shows that enforceEx correctly returns the matched policy rule,
// which may be a role's policy when using role-based access
const e = await newEnforcer('examples/rbac_model.conf', 'examples/rbac_policy.csv');

// Test direct permission - should match alice's direct policy
await testEnforceEx(e, 'alice', 'data1', 'read', [true, ['alice', 'data1', 'read']]);

// Test role-based permission - alice has role data2_admin
// The matched rule should be the role's policy, not alice's
await testEnforceEx(e, 'alice', 'data2', 'read', [true, ['data2_admin', 'data2', 'read']]);
await testEnforceEx(e, 'alice', 'data2', 'write', [true, ['data2_admin', 'data2', 'write']]);

// Test bob's direct permission
await testEnforceEx(e, 'bob', 'data2', 'write', [true, ['bob', 'data2', 'write']]);

// Test failed permission - no matching policy
await testEnforceEx(e, 'bob', 'data2', 'read', [false, []]);
await testEnforceEx(e, 'alice', 'data1', 'write', [false, []]);
});

test('TestEnforceExWithRBACDenyModel', async () => {
const e = await newEnforcer('examples/rbac_with_deny_model.conf', 'examples/rbac_with_deny_policy.csv');
testEnforceEx(e, 'alice', 'data1', 'read', [true, ['alice', 'data1', 'read', 'allow']]);
Expand Down
Loading
Loading