@soudasuwa/permissions
v1.2.2
Published
Generic rule engine and access controller for Node.js
Maintainers
Readme
Rule Engine
A small, generic rule engine for Node.js used to evaluate access control decisions. Rules are expressed as plain objects and checked against a context object. The engine does not assume any specific property names so it can be adapted to a variety of domains.
Overview
The library processes a set of rules to determine whether a user may perform an action. Each rule describes the expected values for attributes in the context and can be combined with logical operators. Comparison and logic handlers are pluggable so you can extend the engine with domain specific behaviour.
Use Cases
- Enforcing who may read, update or delete items in a todo application.
- Authorising collaborative note editing or forum posts.
- Controlling invoice workflows or other business processes.
Example rule sets for these scenarios are provided in the scenarios/ folder.
Quick Start
- Install dependencies:
npm install- Define your rules:
const rules = [
{
when: { resource: "todo", action: "read" },
rule: { "item.ownerId": { reference: "user.id" } },
},
];- Evaluate a context:
// After installing from npm
const { AccessController } = require("@soudasuwa/permissions");
const controller = new AccessController(rules).context({
resource: "todo",
action: "read",
});
const result = controller.pemit({
user: { id: "u1" },
item: { ownerId: "u1" },
});
console.log(result.passed); // trueAccessController also accepts a single rule object instead of an array.
Working with Context
AccessController.context() creates a new controller instance with extra values
merged into the existing context. pemit() evaluates the final context
against the rules. This makes it easy to build up a base context and re-use it
across checks.
const base = new AccessController(rules).context({ resource: "note" });
// Add the action and per-request data before evaluating
const res = base.context({ action: "read" }).pemit({
user: { id: "alice", role: "viewer" },
note: { ownerId: "alice" },
});
console.log(res.passed); // trueRule Examples
The engine can represent many styles of access control. Here are a few common patterns:
Role based
const rbacRules = [
{ when: { action: "delete" }, rule: { "user.role": "admin" } },
{
when: { action: "read" },
rule: { "user.role": { in: ["admin", "viewer"] } },
},
];Attribute based
const abacRules = [
{
when: { action: "update" },
rule: { "item.ownerId": { reference: "user.id" } },
},
];Value checks
const invoiceRules = [
{
when: { resource: "invoice", action: "pay" },
rule: { "invoice.amount": { lessThan: 1000 } },
},
];Existence checks
const existenceRules = [
{ rule: { "user.id": { exists: true } } },
{ rule: { "session.token": { exists: false } } },
];Implicit AND and nested objects
Multiple properties in a rule object are treated as an AND block. Nested
objects expand into dotted paths.
const simpleRule = { resource: "todo", action: "read" };
// Equivalent to: { AND: [{ resource: "todo" }, { action: "read" }] }
const nestedRule = {
user: { id: { exists: true } },
item: { ownerId: { reference: "user.id" } },
};
// Equivalent to:
// {
// "user.id": { exists: true },
// "item.ownerId": { reference: "user.id" }
// }Nested rule groups
const docRules = [
{
when: { resource: "doc" },
rules: [
{ when: { action: "edit" }, rule: { "doc.ownerId": { reference: "user.id" } } },
{ when: { action: "view" }, rule: { "doc.shared": true } },
],
},
];Scenario Examples
The following rule sets expand on the basic patterns above. They are presented
from simplest to most complex and mirror the sample tests in the scenarios/
folder.
Simple ToDo App
const todoRules = [
{
when: { resource: "todo" },
rules: [
{
when: { action: "create" },
rule: { "user.id": { exists: true }, "item.ownerId": { reference: "user.id" } },
},
{ when: { action: "read" }, rule: { "item.ownerId": { reference: "user.id" } } },
{ when: { action: "update" }, rule: { "item.ownerId": { reference: "user.id" } } },
{ when: { action: "delete" }, rule: { "item.ownerId": { reference: "user.id" } } },
],
},
];Friends Tasks
const taskRules = [
{
when: { resource: "task" },
rules: [
{
when: { action: "create" },
rule: { "user.id": { exists: true }, "item.ownerId": { reference: "user.id" } },
},
{
when: { action: "read" },
rule: {
OR: {
"item.ownerId": { reference: "user.id" },
"user.id": { in: { reference: "item.sharedWith" } },
},
},
},
{
when: { action: "update" },
rule: {
OR: {
"item.ownerId": { reference: "user.id" },
"user.id": { in: { reference: "item.sharedWith" } },
},
},
},
{ when: { action: "delete" }, rule: { "item.ownerId": { reference: "user.id" } } },
],
},
];Tic-Tac-Toe with Leaderboard
const gameRules = [
{
when: { resource: "game" },
rules: [
{
when: { action: "create" },
rule: { user: { role: "player", id: { in: { reference: "item.participants" } } } },
},
{
when: { action: "move" },
rule: {
"user.id": { in: { reference: "item.participants" } },
item: { status: { not: "complete" } },
},
},
{
when: { action: "read" },
rule: {
OR: [
{ "item.status": "complete" },
{
user: { id: { in: { reference: "item.participants" } } },
item: { status: { not: "complete" } },
},
],
},
},
],
},
{
when: { resource: "leaderboard" },
rules: [
{ when: { action: "read" }, rule: { "user.role": { in: ["player", "moderator"] } } },
{ when: { action: "update" }, rule: { "user.role": "moderator" } },
],
},
];Collaborative Notes
const noteRules = [
{
when: { resource: "note" },
rules: [
{
when: { action: "create" },
rule: {
OR: {
"notebook.ownerId": { reference: "user.id" },
"user.id": { in: { reference: "notebook.editors" } },
},
},
},
{
when: { action: "read" },
rule: {
OR: [
{ "notebook.ownerId": { reference: "user.id" } },
{ "user.id": { in: { reference: "notebook.editors" } } },
{ "user.id": { in: { reference: "notebook.viewers" } } },
],
},
},
{
when: { action: "update" },
rule: {
OR: {
"notebook.ownerId": { reference: "user.id" },
"user.id": { in: { reference: "notebook.editors" } },
},
},
},
{
when: { action: "delete" },
rule: {
OR: {
"notebook.ownerId": { reference: "user.id" },
"user.id": { in: { reference: "notebook.editors" } },
},
},
},
],
},
{
when: { resource: "notebook" },
rules: [
{ when: { action: "delete" }, rule: { "notebook.ownerId": { reference: "user.id" } } },
{ when: { action: "modifySharing" }, rule: { "notebook.ownerId": { reference: "user.id" } } },
],
},
];Discussion Forum
const forumRules = [
{
when: { resource: "category" },
rules: [
{
when: { action: "view" },
rule: {
OR: {
"category.isPrivate": { not: true },
"user.id": { in: { reference: "category.allowedUsers" } },
"user.role": "admin",
},
},
},
],
},
{
when: { resource: "topic" },
rules: [
{
when: { action: "create" },
rule: {
OR: [
{ user: { role: "member" }, category: { isPrivate: { not: true } } },
{ user: { role: "member", id: { in: { reference: "category.allowedUsers" } } } },
],
},
},
],
},
{
when: { resource: "post" },
rules: [
{
when: { action: "editOwn" },
rule: {
user: { role: "member" },
post: { authorId: { reference: "user.id" }, ageMinutes: { lessThan: 30 } },
},
},
{
when: { action: "editAnyModerator" },
rule: {
user: { role: "moderator", id: { in: { reference: "category.moderators" } } },
},
},
],
},
{ when: { resource: "user", action: "adminDelete" }, rule: { "user.role": "admin" } },
];Invoice Lifecycle
const invoiceRules = [
{
when: { resource: "invoice" },
rules: [
{ when: { action: "view" }, rule: { "user.role": "admin" } },
{
when: { action: "view" },
rule: {
"user.role": "customer",
invoice: {
ownerId: { reference: "user.id" },
status: { in: ["pending", "complete"] },
},
},
},
{
when: { action: "edit" },
rule: {
"user.role": "admin",
invoice: { status: { in: ["draft", "pending"] } },
},
},
{
when: { action: "pay" },
rule: {
"user.role": "customer",
invoice: { ownerId: { reference: "user.id" }, status: "pending" },
},
},
],
},
];Features
- Generic attribute matching – rules reference arbitrary paths within the context.
- Comparison operators – equality,
in,not, valuereference, numeric comparisons (greaterThan,lessThan) andexistschecks. - Logical composition – combine rules with
AND,OR,XORandNOTblocks. - Authorize helper – evaluate arrays of rules with optional
whenconditions. - Nested rule groups – share
whenconditions with child rules using arulesarray. - AccessController – helper class for incrementally building a context.
- Pluggable evaluator – provide custom logic or comparison handlers.
- Functional rule builder – compose rules with helpers like
field,ref,andandnot. - Evaluation trace – inspect which rules triggered via the returned trace array.
Extending
Advanced scenarios may require custom logic, comparison operators or context resolution. The engine already includes AND, OR, XOR and NOT logic. Additional behaviors can be plugged into the DefaultEvaluator:
const nandLogic = {
match: node => typeof node === "object" && node !== null && "NAND" in node,
evaluate: (node, ctx, ev) => {
const items = Array.isArray(node.NAND)
? node.NAND
: Object.entries(node.NAND).map(([k, v]) => ({ [k]: v }));
return !items.every(r => ev.evaluate(r, ctx).passed);
},
};
const controller = new AccessController(rules, {
evaluator: new DefaultEvaluator({ logic: [nandLogic] }),
});Custom comparison handler example
const startsWith = {
match: (_, exp) => typeof exp === "object" && exp !== null && "startsWith" in exp,
evaluate: (attr, exp, ctx) => {
const value = attr.split('.').reduce((o, k) => (o ? o[k] : undefined), ctx);
return typeof value === 'string' && value.startsWith(exp.startsWith);
},
};
const controller = new AccessController(rules, {
evaluator: new DefaultEvaluator({ compare: [startsWith] }),
});Custom context resolver example
const colonResolver = {
resolve: (path, ctx) =>
path.split(":").reduce((o, k) => (o ? o[k] : undefined), ctx),
};
const controller = new AccessController(rules, {
evaluator: new DefaultEvaluator({ contextResolver: colonResolver }),
});Custom rule node handler example
const allowIf = {
match: node => typeof node === "object" && node !== null && "allowIf" in node,
evaluate: (node, ctx, ev) => ev.evaluate(node.allowIf, ctx),
};
const controller = new AccessController(rules, {
evaluator: new DefaultEvaluator({ nodes: [allowIf] }),
});Functional rule builder example
const { field, ref, and, xor, not } = require("./ruleEngine");
const rule = and(
xor(field("user.role", "admin"), field("user.role", "editor")),
not(field("item.status", "archived")),
field("user.id", ref("item.ownerId"))
);
const controller = new AccessController([rule]);
const okCtx = {
user: { id: "u1" },
item: { ownerId: "u1", status: "active" },
};
const result = controller.pemit(okCtx);
console.log(result.passed); // true
// Inspect evaluation trace
console.dir(result, { depth: null });Inspecting evaluation results
AccessController.pemit() returns a trace array describing which rules were
checked. This can be helpful for debugging permissions.
const out = controller.pemit(okCtx);
console.dir(out, { depth: null });
/* Example output:
{
passed: true,
trace: [
{
AND: [
{ "user.id": { reference: "item.ownerId" } },
{ NOT: { "item.status": "archived" } }
]
},
{ NOT: { "item.status": "archived" } },
{ "user.id": { reference: "item.ownerId" } }
]
}
*/Testing
Run the unit tests with:
npm testFormatting and linting can be checked with:
npm run check