the-api-roles
v1.1.2
Published
Roles for the-api
Readme
the-api-roles
Role and permission management for applications built with TheAPI.
the-api-roles helps you:
- define role-to-permission mappings in one place
- inherit permissions between roles
- support wildcard permissions such as
users.*or* - perform owner checks for record-level access
- enforce permissions through middleware or manual checks
Installation
npm install the-api-rolesCore concepts
role: a named group of permissions, such asadminorregisteredpermission: a string such asusers.getorposts.deleteowner check: a permission check that is granted when the current user owns the target object and theownerrole contains the required permission
Quick start
import Roles from 'the-api-roles';
const roles = new Roles({
root: ['*'],
guest: ['auth.login'],
registered: ['_.guest', 'profile.get'],
admin: ['_.registered', 'users.*'],
owner: ['posts.update', 'posts.delete'],
});
roles.append({
editor: ['_.registered', 'posts.create', 'posts.update'],
admin: ['posts.*'],
});Role inheritance
Use the _.roleName syntax to inherit all permissions from another role:
const roles = new Roles({
guest: ['auth.login'],
registered: ['_.guest', 'profile.get'],
admin: ['_.registered', 'users.*'],
});In this example:
registeredinheritsauth.loginadmininherits everything fromregistered
Wildcard permissions
The package supports wildcard permissions:
users.*: any permission in theusersnamespace*: any permission in the system
Example:
const roles = new Roles({
admin: ['users.*'],
root: ['*'],
});Virtual roles
Two roles are typically handled dynamically:
guest: permissions for unauthenticated usersowner: permissions used during owner checks
The owner role is not expected to be present in user.roles. Owner access is determined by comparing:
user.userIdobjectToCheck.userIdby default, or another field defined viaobjectToCheckUserKey
Usage with TheAPI
// roles.ts
import Roles from 'the-api-roles';
const roles = new Roles({
root: ['*'],
guest: ['auth.login'],
registered: ['_.guest', 'profile.get', 'posts.read'],
admin: ['_.registered', 'users.*'],
owner: ['posts.update', 'posts.delete'],
});
roles.append({
editor: ['_.registered', 'posts.create', 'posts.update'],
});
export default roles;// index.ts
import { Routings, TheAPI } from 'the-api';
import roles from './roles';
const router = new Routings();
router.get('/posts', roles.checkPermissionMiddleware('posts.read'));
router.post('/posts', roles.checkPermissionMiddleware('posts.create'));
router.delete('/posts/:id', roles.checkPermissionMiddleware('posts.delete', 'posts'));
router.patch('/posts/:postId', roles.checkPermissionMiddleware('posts.update', 'posts', 'postId'));
router.delete('/posts/:postId/comments/:id', async (c, next) => {
await roles.checkPermission('posts.delete', {
c,
tableName: 'posts',
idParamName: 'postId',
});
await next();
});
const theAPI = new TheAPI({ roles, routings: [router] }); // it will make roles.init() automatically
await theAPI.up();API
new Roles(roleDefinitions)
Creates a new Roles instance and initializes role mappings.
const roles = new Roles({
guest: ['auth.login'],
registered: ['_.guest', 'profile.get'],
admin: ['_.registered', 'users.*'],
owner: ['posts.update', 'posts.delete'],
});Arguments:
roleDefinitions: Record<string, string[]>- role names mapped to permission arrays
roles.init(roleDefinitions?)
Rebuilds the internal permission mapping. Inherited roles are resolved into final permission lookups.
Returns:
Record<string, Record<string, true>>
Example:
roles.init();roles.append(newRoles)
Adds new roles or appends permissions to existing roles, then rebuilds the resolved mapping.
roles.append({
admin: ['posts.*'],
editor: ['_.registered', 'posts.create'],
});Arguments:
newRoles: Record<string, string[]>- additional role definitions
roles.checkPermission(permission, options?)
Checks whether the current user has a permission either through:
- one of the user's roles
- a wildcard permission
- an owner check through the
ownerrole
Returns:
truewhen access is granted
Throws:
Errorwhen access is denied
await roles.checkPermission('posts.delete', {
user: c.var.user,
objectToCheck: c.var.db('posts').where({ id: c.req.param().postId }).first(),
});
await roles.checkPermission('posts.update', {
c,
tableName: 'posts',
idParamName: 'postId',
});Arguments:
permission: string- required permission, for exampleposts.deleteoptions?: object
Options:
c- request context; if provided,useranddbcan be read fromc.varuser- current user object; usuallyc.var.usertableName- table used to load the target object for an owner checkidParamName- route parameter name containing the target record ID; defaults toidobjectToCheck- object or promise resolving to an object for the owner checkobjectToCheckUserKey- owner field name in the target object; defaults touserId
roles.checkPermissionMiddleware(permission, tableName?, idParamName?)
Creates middleware that runs checkPermission() before calling next().
router.delete('/posts/:id', roles.checkPermissionMiddleware('posts.delete', 'posts'));
router.patch('/posts/:postId', roles.checkPermissionMiddleware('posts.update', 'posts', 'postId'));Arguments:
permission: string- required permissiontableName?: string- table used for owner checksidParamName?: string- route parameter name containing the record ID
roles.addRoutePermissions(routePermissions)
Adds route-level permission mappings used by roles.rolesMiddleware().
roles.addRoutePermissions({
'GET /posts': ['posts.read'],
'POST /posts': ['posts.create'],
});roles.rolesMiddleware(c, next)
Checks permissions for matched routes added via addRoutePermissions().
Error handling
When access is denied, the package throws a regular Error:
try {
await roles.checkPermission('posts.delete', { user: c.var.user });
} catch (error) {
if (error instanceof Error) {
console.error(error.message);
}
}Notes
owneris evaluated separately fromuser.roles- wildcard permission matching supports both exact matches and namespace matches such as
posts.* - cyclic role inheritance throws an error during
init()
