@nothinghere/rbac
v1.0.2
Published
Role-Based Access Control (RBAC) and Attribute-Based Access Control (ABAC) library for Node.js
Maintainers
Readme
@nothinghere/RBAC
Installation
npm install rbacQuick start
make sure you set type to module in package.json
{
"type": "module"
}import {
createRBAC,
defineRoles,
defineSchema,
mergeResources,
defineResource,
} from "@nothinghere/rbac";
const schema = defineSchema(
mergeResources(
defineResource("post", {
create: null, // no context needed
read: null,
edit: {} as { userId: string; ownerId: string }, // context required
delete: null,
}),
defineResource("comment", {
create: null,
delete: {} as { userId: string; ownerId: string },
}),
),
);
const roles = defineRoles(schema, {
guest: {
allow: ["post:read"],
},
user: {
allow: [
"post:create",
"comment:create",
{
permission: "post:edit",
when: async (ctx) => ctx.userId === ctx.ownerId,
},
{
permission: "comment:delete",
when: async (ctx) => ctx.userId === ctx.ownerId,
},
],
inherits: ["guest"],
},
moderator: {
allow: ["post:delete", "comment:delete"],
inherits: ["user"],
},
admin: {
allow: ["*"], // grant everything …
deny: ["post:delete"], // … except post deletion
},
});
const rbac = createRBAC({ roles });
const result = await rbac.can("user", "post:edit", {
userId: "1",
ownerId: "1",
});
if (result.allowed) {
// proceed
} else {
console.log(result.reason); // 'no_matching_rule' | 'explicitly_denied' | ...
}API
defineSchema(schema)
Declares the full set of permissions and the context type each one requires. Values are either null (no context) or a typed placeholder.
const schema = defineSchema({
"post:create": null,
"post:edit": {} as { userId: string; ownerId: string },
});defineResource(namespace, permissions)
A convenience helper that namespaces a group of permissions as "namespace:key". Combine with mergeResources for large codebases.
const postPerms = defineResource("post", {
create: null,
edit: {} as { userId: string; ownerId: string },
});
const commentPerms = defineResource("comment", { create: null });
const schema = defineSchema(mergeResources(postPerms, commentPerms));
// schema keys: 'post:create', 'post:edit', 'comment:create'mergeResources(...resources)
Merges multiple resource schemas into one. Throws at runtime if any two resources declare the same key.
const schema = defineSchema(mergeResources(postPerms, commentPerms, userPerms));defineRoles(schema, rolesConfig)
Declares roles with typed allow, optional deny, and optional inherits arrays. TypeScript enforces that every entry refers to a valid schema key and that when() receives the correct context type.
const roles = defineRoles(schema, {
user: {
allow: [
"post:create",
{
permission: "post:edit",
when: async (ctx) => ctx.userId === ctx.ownerId,
},
],
deny: ["post:delete"],
inherits: ["guest"],
},
});allow entries
| Form | Meaning |
| ---------------------- | -------------------------------------------------------------------- |
| '*' | Grant every permission unconditionally |
| 'post:*' | Grant every permission whose name starts with post: |
| 'post:create' | Grant a specific permission unconditionally |
| { permission, when } | Grant a specific permission only when when(ctx) resolves to true |
deny entries
| Form | Meaning |
| ---------------------- | ------------------------------------------------------------- |
| 'post:delete' | Always deny this permission |
| { permission, when } | Deny this permission only when when(ctx) resolves to true |
Wildcards are not allowed in
denyentries — denials must always be explicit.
inherits
A role can inherit from one or more parent roles. The full ancestry is flattened and evaluated as a single rule set. Deny rules from any role in the ancestry always win, regardless of which role has an allow for the same permission.
createRBAC({ roles })
Creates the RBAC instance. Validates all inheritance chains for circular references at construction time — misconfiguration is caught at startup rather than inside a request.
const rbac = createRBAC({ roles });rbac.can(role, permission, context?)
Evaluates whether a role (or set of roles) has access to a permission.
// Single role, no context needed
const result = await rbac.can("guest", "post:read");
// Single role, context required
const result = await rbac.can("user", "post:edit", {
userId: req.user.id,
ownerId: post.ownerId,
});
// Multiple roles — access is granted if any role allows it,
// but a deny from any role still blocks access
const result = await rbac.can(["user", "moderator"], "post:delete");Returns Promise<RBACResult>:
type RBACResult = { allowed: true } | { allowed: false; reason: DenyReason };
type DenyReason =
| "role_not_found"
| "permission_not_found"
| "explicitly_denied"
| "condition_failed" // a when() guard threw an error
| "no_matching_rule"; // no allow rule matchedEvaluation order
For each call to can():
- Flatten the role's full ancestry (breadth-first, deduped).
- Check deny rules across all flattened roles — static first, then conditional
when(). A single matching deny rule immediately returns{ allowed: false, reason: 'explicitly_denied' }. - Check allow rules — static matches first (including wildcards), then conditional
when(). The first matching rule returns{ allowed: true }. - If nothing matched, return
{ allowed: false, reason: 'no_matching_rule' }.
When multiple roles are passed as an array, all their ancestries are merged into one set before evaluation. A deny rule from role A overrides an allow rule from role B.
Scaling with defineResource
For large applications, define permissions per domain module and compose them at the top level:
export const postPerms = defineResource('post', {
create: null,
read: null,
edit: {} as { userId: string; ownerId: string },
delete: null,
});
export const commentPerms = defineResource('comment', {
create: null,
delete: {} as { userId: string; ownerId: string },
});
import { mergeResources, defineSchema } from 'rbac';
import { postPerms } from './post.js';
import { commentPerms } from './comment.js';
export const schema = defineSchema(mergeResources(postPerms, commentPerms));
import { defineRoles } from 'rbac';
import { schema } from '../permissions/index.js';
export const roles = defineRoles(schema, { ... });License
ISC
