This is an automated email from the ASF dual-hosted git repository.
hsluoyz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/casbin-casbin.js.git
The following commit(s) were added to refs/heads/master by this push:
new 7535621 fix: support CasbinJsGetPermissionForUser output in
setPermission
7535621 is described below
commit 7535621be93f5c57777480ed30b643813d4a5d47
Author: Yang Luo <[email protected]>
AuthorDate: Thu Aug 6 18:29:05 2026 +0800
fix: support CasbinJsGetPermissionForUser output in setPermission
---
README.md | 17 ++++++++
src/Authorizer.ts | 91 +++++++++++++++++++++++++++++++----------
src/Permission.ts | 2 +-
src/__test__/enforcer.test.ts | 43 ++++++++++++++++++-
src/__test__/permission.test.ts | 12 ++++--
5 files changed, 139 insertions(+), 26 deletions(-)
diff --git a/README.md b/README.md
index d5f3c9d..24776d1 100644
--- a/README.md
+++ b/README.md
@@ -44,6 +44,23 @@ authorizer.cannot("write", "data2").then(result => {
});
```
+In `manual` mode you can also pass the model and policies produced by the
backend's
+`CasbinJsGetPermissionForUser` (a JSON object with the keys `m`, `p` and `g`)
to
+`setPermission`. Casbin.js then builds a real enforcer from that data, so
roles (`g`
+rules) are taken into account. Remember to set the user, since the request is
evaluated
+as `(user, object, action)`.
+```javascript
+const authorizer = new casbinjs.Authorizer("manual");
+
+// The JSON string or object returned by CasbinJsGetPermissionForUser
+authorizer.setPermission(responseFromApi);
+await authorizer.setUser("alice");
+
+authorizer.can("read", "data1").then(result => {
+ console.log(result)
+});
+```
+
You can also use the `auto` mode. In details, specify a casbin backend service
endpoint when initializing the Casbin.js authorizer, and set the subject when
the frontend user identity changes. Casbin.js will automatically fetch the
permission from the endpoint. (A pre-configurated casbin service API is
required at the backend.)
```javascript
const casbinjs = require('casbin.js');
diff --git a/src/Authorizer.ts b/src/Authorizer.ts
index a533a49..86b8a85 100644
--- a/src/Authorizer.ts
+++ b/src/Authorizer.ts
@@ -10,6 +10,14 @@ interface BaseResponse {
type Mode = "auto" | "cookies" | "manual"
+/**
+ * Whether the object is the model + policies produced by casbin's
+ * "CasbinJsGetPermissionForUser", rather than an action-to-objects map.
+ */
+function isEnforcerData(obj: unknown): obj is Record<string, unknown> {
+ return typeof obj === 'object' && obj !== null && typeof (obj as
Record<string, unknown>)['m'] === 'string';
+}
+
export class Authorizer {
public mode!: Mode;
public endpoint: string | undefined = undefined;
@@ -19,6 +27,7 @@ export class Authorizer {
public cacheExpiredTime = 60; // Seconds
public user : string | undefined;
public enforcer:casbin.Enforcer | undefined;
+ private enforcerPromise: Promise<void> | undefined;
/**
*
@@ -75,30 +84,63 @@ export class Authorizer {
}
}
+ /**
+ * Load the permission data.
+ *
+ * Two shapes are accepted:
+ * - A map of actions to objects, e.g. {"read": ["data1", "data2"],
"write": ["data1"]}
+ * - The model and policies returned by casbin's
"CasbinJsGetPermissionForUser"
+ * (a JSON object with the keys "m", "p" and "g"). In this case an
enforcer is
+ * built from that data, and "can" evaluates the model instead of a
static map.
+ */
public setPermission(permission : Record<string, unknown> | string) : void{
+ const obj = typeof permission === 'string' ? JSON.parse(permission) :
permission;
+ if (isEnforcerData(obj)) {
+ this.enforcerPromise = this.initEnforcer(obj);
+ // The rejection is surfaced when "can" awaits the promise, this
only
+ // keeps it from being reported as an unhandled rejection
meanwhile.
+ this.enforcerPromise.catch(() => undefined);
+ return;
+ }
if (this.permission === undefined) {
this.permission = new Permission();
}
- this.permission.load(permission);
+ this.permission.load(obj);
}
- public async initEnforcer(s: string): Promise<void> {
- const obj = JSON.parse(s);
+ public async initEnforcer(s: string | Record<string, unknown>):
Promise<void> {
+ const obj = typeof s === 'string' ? JSON.parse(s) : s;
if (!('m' in obj)) {
throw Error("No model when init enforcer.");
}
const m = new casbin.Model(obj['m']);
this.enforcer = await casbin.newEnforcer(m);
- if ('p' in obj) {
- for (const sArray of obj['p']) {
- let arr = sArray as string[];
- arr = arr.map(v => v.trim())
- const pType = arr.shift()
- if (pType == 'p'){
- await this.enforcer.addPolicy(...arr);
- } else if (pType == 'g'){
- await this.enforcer.addGroupingPolicy(...arr);
- }
+ // "CasbinJsGetPermissionForUser" puts the policies under "p" and the
grouping
+ // policies under "g", each rule prefixed with its own ptype.
+ for (const section of ['p', 'g']) {
+ if (section in obj) {
+ await this.loadRules(obj[section]);
+ }
+ }
+ }
+
+ private async loadRules(rules: unknown): Promise<void> {
+ if (!Array.isArray(rules)) {
+ return;
+ }
+ for (const rule of rules) {
+ if (!Array.isArray(rule)) {
+ continue;
+ }
+ const arr = (rule as string[]).map(v => v.trim());
+ const pType = arr.shift();
+ if (!pType) {
+ continue;
+ }
+ if (pType.startsWith('g')) {
+ await (this.enforcer as
casbin.Enforcer).addNamedGroupingPolicy(pType, ...arr);
+ } else {
+ await (this.enforcer as casbin.Enforcer).addNamedPolicy(pType,
...arr);
}
}
}
@@ -125,8 +167,11 @@ export class Authorizer {
* @param user The current user
*/
public async setUser(user : string) : Promise<void> {
- if (this.mode == 'auto' && user !== this.user) {
- this.user = user;
+ if (user === this.user) {
+ return;
+ }
+ this.user = user;
+ if (this.mode == 'auto') {
let config = Cache.loadFromLocalStorage(user);
if (config === null) {
config = await this.getEnforcerDataFromSvr();
@@ -137,16 +182,20 @@ export class Authorizer {
}
public async can(action: string, object: string, domain?: string):
Promise<boolean> {
- if (this.mode == "manual") {
- return this.permission !== undefined &&
this.permission.check(action, object);
- } else if (this.mode == "auto") {
- if (this.enforcer === undefined) {
- throw Error("Enforcer not initialized");
- } else if (domain == undefined) {
+ if (this.enforcerPromise !== undefined) {
+ await this.enforcerPromise;
+ }
+ if (this.enforcer !== undefined) {
+ if (domain == undefined) {
return await this.enforcer.enforce(this.user, object, action);
} else {
return await this.enforcer.enforce(this.user, domain, object,
action);
}
+ }
+ if (this.mode == "manual") {
+ return this.permission !== undefined &&
this.permission.check(action, object);
+ } else if (this.mode == "auto") {
+ throw Error("Enforcer not initialized");
} else {
throw Error(`Mode ${this.mode} not recognized.`);
}
diff --git a/src/Permission.ts b/src/Permission.ts
index ba2cd42..4e3b947 100644
--- a/src/Permission.ts
+++ b/src/Permission.ts
@@ -25,7 +25,7 @@ export default class Permission {
// Generate data: {key:Objects, value: Array of actions}
const tmp : StringKV = {};
for (const act in p) {
- for (const obj in p[act]) {
+ for (const obj of p[act]) {
if (!(obj in tmp)) {
tmp[obj] = [];
}
diff --git a/src/__test__/enforcer.test.ts b/src/__test__/enforcer.test.ts
index 4dbf36d..3703d23 100644
--- a/src/__test__/enforcer.test.ts
+++ b/src/__test__/enforcer.test.ts
@@ -1,6 +1,6 @@
import * as casbin from "casbin-core"
import { Authorizer } from "../Authorizer";
-import { basicModelStr, rbacWithDomainsModelStr} from './models';
+import { basicModelStr, rbacModelStr, rbacWithDomainsModelStr} from './models';
const respData = JSON.stringify({
m: basicModelStr,
@@ -56,6 +56,47 @@ test('Authorizer enforcer with domain API', async() => {
expect(await authorizer.canAll("write", ["data1", "data2"],
"domain1")).toBe(false);
})
+// The shape returned by casbin's "CasbinJsGetPermissionForUser": the grouping
+// policies live under their own "g" key, not inside "p".
+const respDataWithSeparateGroupingRules = {
+ m: rbacModelStr,
+ p: [
+ ["p", "admin", "data1", "read"],
+ ["p", "admin", "data1", "write"],
+ ["p", "user", "data1", "read"],
+ ],
+ g: [
+ ["g", "alice", "admin"],
+ ["g", "bob", "user"],
+ ],
+};
+
+test('Authorizer enforcer with grouping policies in the "g" section', async()
=> {
+ const authorizer = new Authorizer("auto", {endpoint: "whatever"});
+ await
authorizer.initEnforcer(JSON.stringify(respDataWithSeparateGroupingRules));
+ authorizer.user = "alice";
+ expect(await authorizer.can("read", "data1")).toBe(true);
+ expect(await authorizer.can("write", "data1")).toBe(true);
+ authorizer.user = "bob";
+ expect(await authorizer.can("read", "data1")).toBe(true);
+ expect(await authorizer.can("write", "data1")).toBe(false);
+})
+
+test('Manual mode accepts the model and policies of
CasbinJsGetPermissionForUser', async() => {
+ const authorizer = new Authorizer("manual");
+ authorizer.setPermission(respDataWithSeparateGroupingRules);
+ await authorizer.setUser("alice");
+ expect(await authorizer.can("read", "data1")).toBe(true);
+ expect(await authorizer.can("write", "data1")).toBe(true);
+ expect(await authorizer.can("read", "data2")).toBe(false);
+
+ const fromString = new Authorizer("manual");
+
fromString.setPermission(JSON.stringify(respDataWithSeparateGroupingRules));
+ await fromString.setUser("bob");
+ expect(await fromString.can("read", "data1")).toBe(true);
+ expect(await fromString.cannot("write", "data1")).toBe(true);
+})
+
const s = `[request_definition]
r = sub, obj, act
diff --git a/src/__test__/permission.test.ts b/src/__test__/permission.test.ts
index 2639fb5..b5cd33e 100644
--- a/src/__test__/permission.test.ts
+++ b/src/__test__/permission.test.ts
@@ -35,11 +35,17 @@ describe('Permission unit test', () => {
})
test('Get action-object mapping', () => {
- // TODO
+ permission.load(permExample);
+ const actual = permission.getActionsObjects();
+ expect(actual.get('read')).toEqual(['data1', 'data2']);
+ expect(actual.get('write')).toEqual(['data2']);
});
-
+
test('Get object-action mapping', () => {
- // TODO
+ permission.load(permExample);
+ const actual = permission.getObjectsActions();
+ expect(actual.get('data1')).toEqual(['read']);
+ expect(actual.get('data2')).toEqual(['read', 'write']);
})
})
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]