A TypeScript Access Control List module with memory and Redis backend support
npm install @teleworld/aclA modern, TypeScript-first ACL implementation inspired by Zend_ACL with dual ESM/CommonJS output.
This is a complete rewrite of the original ACL library with modern TypeScript:
- Full TypeScript support with strict typing and excellent IDE support
- Dual package output - works with both CommonJS and ESM
- Contract-free - replaced runtime contract checking with TypeScript types
- MongoDB support removed - focus on memory and Redis backends only
- Modern tooling - built with Rollup, tested with Vitest
- Zero dependencies except lodash for utilities
When you develop a website or application you will soon notice that sessions are not enough to protect all the
available resources. Avoiding that malicious users access other users content proves a much more
complicated task than anticipated. ACL can solve this problem in a flexible and elegant way.
Create roles and assign roles to users. Sometimes it may even be useful to create one role per user,
to get the finest granularity possible, while in other situations you will give the _asterisk_ permission
for admin kind of functionality.
Memory and Redis backends are provided built-in. This version focuses on these two most commonly used backends for better maintainability and performance.
- Users
- Roles
- Hierarchies
- Resources
- Express middleware for protecting resources.
- Robust implementation with good unit test coverage.
Using npm:
``shell script`
npm install @teleworld/acl
For Redis backend:
`shell script`
npm install redis
- addUserRoles
- removeUserRoles
- userRoles
- roleUsers
- hasRole
- addRoleParents
- removeRoleParents
- removeRole
- removeResource
- allow
- removeAllow
- allowedPermissions
- isAllowed
- areAnyRolesAllowed
- whatResources
- middleware
- backend
Create your acl module by importing it and instantiating it with a valid backend instance:
`typescript
import { Acl, MemoryBackend, RedisBackend } from '@teleworld/acl';
// Using Redis backend
const acl = new Acl(new RedisBackend({ redis: redisClient }));
// Or using the memory backend
const acl = new Acl(new MemoryBackend());
`
`javascript
const { Acl, MemoryBackend, RedisBackend } = require('@teleworld/acl');
// Using Redis backend
const acl = new Acl(new RedisBackend({ redis: redisClient }));
// Or using the memory backend
const acl = new Acl(new MemoryBackend());
`
`typescript
import { Acl, memoryBackend, redisBackend } from '@teleworld/acl';
// Using factory functions
const acl = new Acl(memoryBackend());
const aclRedis = new Acl(redisBackend({ redis: redisClient }));
`
See below for full list of backend constructor arguments.
All the following functions return a promise.
Create roles implicitly by giving them permissions:
`javascript
// guest is allowed to view blogs
await acl.allow("guest", "blogs", "view");
// allow function accepts arrays as any parameter
await acl.allow("member", "blogs", ["edit", "view", "delete"]);
`
Users are likewise created implicitly by assigning them roles:
`javascript`
await acl.addUserRoles("joed", "guest");
Hierarchies of roles can be created by assigning parents to roles:
`javascript`
await acl.addRoleParents("baz", ["foo", "bar"]);
Note that the order in which you call all the functions is irrelevant (you can add parents first and assign permissions to roles later)
`javascript`
await acl.allow("foo", ["blogs", "forums", "news"], ["view", "delete"]);
Use the wildcard to give all permissions:
`javascript`
await acl.allow("admin", ["blogs", "forums"], "*");
Sometimes is necessary to set permissions on many different roles and resources. This would
lead to unnecessary nested callbacks for handling errors. Instead use the following:
`javascript`
await acl.allow([
{
roles: ["guest", "member"],
allows: [
{ resources: "blogs", permissions: "get" },
{ resources: ["forums", "news"], permissions: ["get", "put", "delete"] },
],
},
{
roles: ["gold", "silver"],
allows: [
{ resources: "cash", permissions: ["sell", "exchange"] },
{ resources: ["account", "deposit"], permissions: ["put", "delete"] },
],
},
]);
You can check if a user has permissions to access a given resource with _isAllowed_:
`javascript`
const res = await acl.isAllowed("joed", "blogs", "view");
if (res) {
console.log("User joed is allowed to view blogs");
}
Of course arrays are also accepted in this function:
`javascript`
await acl.isAllowed("jsmith", "blogs", ["edit", "view", "delete"]);
Note that all permissions must be fulfilled in order to get _true_.
Sometimes is necessary to know what permissions a given user has over certain resources:
`javascript`
const permissions = await acl.allowedPermissions("james", ["blogs", "forums"]);
console.log(permissions);
It will return an array of resource:[permissions] like this:
`javascript`
[{ blogs: ["get", "delete"] }, { forums: ["get", "put"] }];
Finally, we provide a middleware for Express for easy protection of resources.
`javascript`
acl.middleware();
We can protect a resource like this:
`javascript`
app.put('/blogs/:id', acl.middleware(), function(req, res, next){…}
The middleware will protect the resource named by _req.url_, pick the user from _req.session.userId_ and check the permission for _req.method_, so the above would be equivalent to something like this:
`javascript`
await acl.isAllowed(req.session.userId, "/blogs/12345", "put");
The middleware accepts 3 optional arguments, that are useful in some situations. For example, sometimes we
cannot consider the whole url as the resource:
`javascript`
app.put('/blogs/:id/comments/:commentId', acl.middleware(3), function(req, res, next){…}
In this case the resource will be just the three first components of the url (without the ending slash).
It is also possible to add a custom userId or check for other permissions than the method:
`javascript`
app.put('/blogs/:id/comments/:commentId', acl.middleware(3, 'joed', 'post'), function(req, res, next){…}
Adds roles to a given user id.
Arguments
`javascript`
userId {String} User id.
roles {String|Array} Role(s) to add to the user id.
---
Remove roles from a given user.
Arguments
`javascript`
userId {String} User id.
roles {String|Array} Role(s) to remove to the user id.
---
Return all the roles from a given user.
Arguments
`javascript`
userId {String} User id.
---
Return all users who has a given role.
Arguments
`javascript`
roleName {String} User id.
---
Return boolean whether user has the role
Arguments
`javascript`
userId {String} User id.
roleName {String} role name.
---
Adds a parent or parent list to role.
Arguments
`javascript`
role {String} Child role.
parents {String|Array} Parent role(s) to be added.
---
Removes a parent or parent list from role.
If parents is not specified, removes all parents.
Arguments
`javascript`
role {String} Child role.
parents {String|Array} Parent role(s) to be removed [optional].
---
Removes a role from the system.
Arguments
`javascript`
role {String} Role to be removed
---
Removes a resource from the system
Arguments
`javascript`
resource {String} Resource to be removed
---
Adds the given permissions to the given roles over the given resources.
Arguments
`javascript`
roles {String|Array} role(s) to add permissions to.
resources {String|Array} resource(s) to add permisisons to.
permissions {String|Array} permission(s) to add to the roles over the resources.
Arguments
`javascript`
permissionsArray {Array} Array with objects expressing what permissions to give.
[{roles:{String|Array}, allows:[{resources:{String|Array}, permissions:{String|Array}]]
---
Remove permissions from the given roles owned by the given role.
Note: we loose atomicity when removing empty role_resources.
Arguments
`javascript`
role {String}
resources {String|Array}
permissions {String|Array}
---
Returns all the allowable permissions a given user have to
access the given resources.
It returns an array of objects where every object maps a
resource name to a list of permissions for that resource.
Arguments
`javascript`
userId {String} User id.
resources {String|Array} resource(s) to ask permissions for.
---
Checks if the given user is allowed to access the resource for the given
permissions (note: it must fulfill all the permissions).
Arguments
`javascript`
userId {String} User id.
resource {String} resource to ask permissions for.
permissions {String|Array} asked permissions.
---
Returns true if any of the given roles have the right permissions.
Arguments
`javascript`
roles {String|Array} Role(s) to check the permissions for.
resource {String} resource to ask permissions for.
permissions {String|Array} asked permissions.
---
Returns what resources a given role has permissions over.
Arguments
`javascript`
role {String|Array} Roles
whatResources(role, permissions) : resources
Returns what resources a role has the given permissions over.
Arguments
`javascript`
role {String|Array} Roles
permissions {String|Array} Permissions
---
Middleware for express.
To create a custom getter for userId, pass a function(req, res) which returns the userId when called (must not be async).
Arguments
`javascript`
numPathComponents {Number} number of components in the url to be considered part of the resource name.
userId {String} the user id for the acl system (defaults to req.session.userId)
permissions {String|Array} the permission(s) to check for (defaults to req.method.toLowerCase())
---
Creates an in-memory backend instance. This backend stores all data in memory and is suitable for development, testing, or applications with small datasets.
Arguments
None required.
Example:
`typescript`
import { Acl, MemoryBackend } from '@teleworld/acl';
const acl = new Acl(new MemoryBackend());
Creates a Redis backend instance.
Arguments
`typescript`
interface RedisBackendOptions {
redis: RedisClient; // Redis client instance
prefix?: string; // Optional prefix. Default is "acl_"
}
Example:
`typescript
import { createClient } from 'redis';
import { Acl, RedisBackend } from '@teleworld/acl';
const client = await createClient({ url: 'redis://127.0.0.1:6379' }).connect();
const acl = new Acl(new RedisBackend({ redis: client, prefix: 'my_acl_' }));
`
Run tests with npm or pnpm. For Redis tests, requires a local Redis server running.
`shell script`
npm test
You can run tests for specific backends:
`shell script`
npm run test:memory
npm run test:redis
Or run tests in UI mode:
`shell script`
npm run test:ui
`shell scriptBuild the project
npm run build