casbin-client is a library which facilitates manipulation, management, and storage of user permissions in a frontend application for the purposes of authorization. It supports various access control policies, like RBAC, ABAC, ACL, etc.
It is primarily a library for Casbin and strives to be a more modern and polymorphic alternative to the official Casbin.js client library; it is a complete rewrite from the ground up, sharing zero code with its predecessor. It can and will work without any dependencies, however, so having any knowledge of Casbin is entierly optional.
Due to highly modular structure, both complexity and size requirements can grow dynamically as needed.
// simple
const user = { can: authorizer(() => permissions) };
user.can('read', 'data');
// with caching
const user = createAuthorizer(() => permissions, { store: sessionStorage });
// and/or promises
const user = createAuthorizer(Promise.resolve(permissions), { store: sessionStorage });
user.can('read', 'data');
// with full casbin model and policy parsing
const user = createAuthorizer(() => (
fromPolicySource(policy, { parseExpression })
), { store: sessionStorage });
user.can('read', 'data');| Feature | casbin-client |
casbin.js |
|---|---|---|
| 🌟 Modern tech-stack and dev practices | ✅ TypeScript, DI, FP | 🥀 Babel, OOP |
| 🏝️ Less external dependencies | ✅ Zero-dependencies version available | 🥀 Mandatory axios, babel, casbin-core |
| 💻 Ergonomic development experience | ✅ Import and use how you like | 🥀 Use in compliance with assumptions hidden in source code |
| 🪄 Support for various runtime modes | ✅ Supports both regular (sync) and async modes | 🥀 Every method is async |
| 🪶 Lightweight and tree-shakeable | ✅ 0.5KB↔8KB, take what you need | 🥀 90KB+, no tree-shaking |
| 🔌 Extendable | ✅ Pluginable at every step | 🥀 Depend on implementation details |
| 🤝 Type-safe | ✅ Use typed policies to enforce type safety | 🥀 Untyped strings only |
| 🌐 Environment-independent | ✅ Works in any modern JS environment | 🥀 CommonJS build only |
| ⚙️ Reliability | ✅ |
🥀 No tests... |
| 🔃 More to come... |
npm i casbin-client
# or
bun add casbin-clientAt the centrepoint is the concept of an Authorizer - a singleton that looks at users' permissions and decides if the "user" can or cannot do certain actions:
import { createAuthorizer } from 'casbin-client';
const permissions = {
read: ['data']
};
const user.can = createAuthorizer(() => permissions);
if (user.can('read', 'data')) {
console.log('Yay, we can read data!');
}
//...createAuthorizer takes a simple Permissions factory as its primary argument and provides a semantic interface to read from it.
It never modifies or tampers with the original object, acting like a simple view on it.
If permissions need changing, simply update them:
//...
if (!user.can('read', 'users')) {
console.log('Oops, wrong permissions!');
}
permissions.read = ['data', 'users'];
if (user.can('read', 'users')) {
console.log('Yay, we can read users!');
}And that's the basics!
There are 5 isolated modules:
casbin-client/core- the tiny "core" of the package with a single purpose - to create an authorizercasbin-client- exports a multi-purpose factory for advanced usescasbin-client/model- parser for Casbin modelscasbin-client/policy- parser for Casbin policiescasbin-client/parser- parser for Casbin expressions
Each module is independent from others, and thus very has little effect on the final bundle size of your application.
As covered in the basics section, casbin-client exports a simple createAuthorizer function, with some helper types.
But what if even this is too much?
Enter, casbin-client/core:
import { authorizer, type Permissions } from 'casbin-client/core';
const permissions = {
read: ['data', 'users'] as const
} satisfies Permissions; // enables full autocomplete
const can = authorizer(() => permissions);
if (can('read', 'data')) {
console.log('Yay, we can read data!');
}
// Logs "Yay, we can read data!"It accepts a simple AuthorizerOptions object as its second argument:
import { type AuthorizerOptions } from 'casbin-client/core';
const options = {
fallback: (action, object) => object !== 'database' && action !== 'delete',
// A fallback function to resolve missing permissions
};
const can = authorizer(() => permissions);
if (can('delete', 'database')) {
console.log('We are doomed!');
} else {
console.log('Phew, we are safe.');
}
// Logs "Phew, we are safe."This is a much more versatile factory function.
It allows automatic caching using the Storage API and working with promises.
createAuthorizer also accepts two arguments:
- a
Permissionsobject:import { type Permissions } from 'casbin-client'; const permissions: Permissions = { read: ['data', 'users'] };
- and customization options (optional)
import { type SyncAuthorizerOptions } from 'casbin-client'; const options: SyncAuthorizerOptions = { store: sessionStorage, // A `Storage` object to use as a cache for permissions key: 'auth', // A unique key to store the permissions in the store fallback: (action, object) => object !== 'database' && action !== 'delete', // A fallback function to resolve missing permissions };
And allows for simple permission checking:
import { createAuthorizer } from 'casbin-client';
const user = createAuthorizer(() => permissions, options);
if (user.can('delete', 'database')) {
console.log('We are doomed!');
} else {
console.log('Phew, we are safe.');
}
// Logs "Phew, we are safe."Note
The
.canmethod always re-runs the permission factory!
In reactive UI-frameworks it is advised to wrap its calls with a computed primitive, likeuseMemoorcomputed.
Note
This mode is not for usage with reactive UI frameworks like
react,solid, orvue.
In the context of reactive data in UI components, it's better to usecreateAuthorizerin combination with reactive primitives likeuseQuery,createResource, orcomputed.The "Async mode" is for the case when there's no way to use a reactive primitive and the execution context is synchronous.
createAuthorizer makes it easy to work with promises, because the permissions factory can also be a promise:
const permissionsUrl = 'https://raw.githubusercontent.com/Raiondesu/casbin-client/refs/heads/main/examples/permissions.json';
const remotePermissions = fetch(permissionsUrl).then(r => r.json());createAuthorizer simply treats the promise as a factory:
const user = createAuthorizer(remotePermissions, options);
// ...
// some time later in a file far far away
if (user.can('read', 'data')) {
console.log('Yay, we can read data!');
}In the context of a single function this is, of course, not possible, so the promise is proxied and can be awaited separately:
await user.remote;
if (user.can('read', 'data')) {
console.log('Yay, we can read data!');
}Both authorizer and createAuthorizer accept a generic parameter, which can be automatically inferred from permissions:
type MyPermissions = {
read: ['data']
};
const permissions: any = {
read: ['data']
};
const auth = createAuthorizer<MyPermissions>(() => permissions);
// Full autocomplete and type checking!
auth.can('read', 'data');Allows to parse and use a Casbin model.
import { parseModel } from 'casbin-client/model';
const model = `
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.obj == p.obj && r.act == p.act && g(r.sub, p.sub)
`;
const parsed = parseModel(model);
console.log(parsed.matchers.m({
r: { sub: 'alice', act: 'read', obj: 'data' },
p: { sub: 'reader', act: 'read', obj: 'data' },
g: (r, p) => 'alice' === r && 'reader' === p,
...parsed.matchers,
...parsed.policyEffect
}));
//> trueAllows to parse and use a Casbin model with a Casbin policy.
This module implements the most essential sub-set of read-only features from casbin-core.
See the list of missing features to gauge if this is useful for your project.
import { createAuthorizer } from 'casbin-client';
import { fromPolicySource } from 'casbin-client/policy';
const model = `
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.obj == p.obj && r.act == p.act && g(r.sub, p.sub)
`;
// Result from `CasbinJsGetUserPermission` or otherwise manually loaded
const policy = {
g: [
["g", "alice", "reader"],
["g", "alice", "writer"],
["g", "bob", "reader"],
["g", "cathy", "admin"],
],
m: model,
p: [
["p", "reader", "data", "read"],
["p", "writer", "data", "write"],
["p", "admin", "data", "delete"],
]
};
// Note that this is a costly function to call
const permissions = fromPolicySource(policy);
const user = createAuthorizer(() => permissions);
if (user.can('read', 'data')) {
console.log('Yay, we can read data!');
}
const alicePermissions = fromPolicySource(policy, {
request: ['r', 'alice']
});
const alice = createAuthorizer(() => alicePermissions);
if (alice.can('read', 'data')) {
console.log('Yay, alice can read data!');
}
if (!alice.can('delete', 'data')) {
console.log('Nope, alice cannot delete data!');
}This module uses modified subscript with a subset of justin syntax.
import { parseExpression } from 'casbin-client/parser';
const run = parseExpression('"a" in b && b.a() === true');
console.log(run({ b: { a: () => true } }));
//> trueIt can be passed into model and policy parsers as options, in order to enable complete Casbin experience in JS:
const reader = fromPolicySource(policy, {
request: ['r', 'bob'],
parseExpression,
});
const bob = createAuthorizer(() => reader);
if (bob.can.read('data')) {
console.log('Yeah, Bob can read');
}Casbin is amazing for dynamic and polymorphic control of user access. But the official client-side library left a lot to be desired. Being a de-facto extension on the casbin-core package for Node.js, it brings in a lot of unneeded dependencies and wraps them in an API that is awkward to use in a modern JS ecosystem.
- Process simple policies (
{ write: ['data'], read: ['data'] }) - Custom storage or DB providers for caching
- Simple integration with any network/query client
- Ability to check user permissions using policies and model matchers
- Ability to parse permissions from policies without the baggage of matchers and effects
- Integrations for popular frontend frameworks
- Generate ambient types from policy csv or permissions json
- Parse permissions at the type level from policy source
- Reliable error reporting
- Support for complex pattern-matching (
/data/*,keyMatch(...)) - Support for internal
eval(...)and other built-in functions - Support for custom matcher contexts
- Support for effect expressions
- Full test coverage
Feel like something's missing? Submit an issue!
Wanna help? Fork and submit a PR!
Parsing model configuration leads to evaluation of user-provided expressions, which can lead to unsafe behavior. Refrain from using arbitrary model parsing on the client-side to avoid potential security risks!
Note
Despite lacking a similar warning, Casbin.js has the same potential for introducing vulnerabilities.
Prerequisites:
bun:curl -fsSL https://bun.sh/install | bashpowershell -c "irm bun.sh/install.ps1 | iex"
To install dependencies:
bun installbun run buildbun run test