True Attribute-Based Access Control (ABAC) engine for Node.js - Authorization decisions based purely on attributes
npm install abac-engineA powerful, zero-dependency Attribute-Based Access Control (ABAC) engine for
Node.js and TypeScript. Make authorization decisions based on attributes instead
of roles.
Traditional access control uses roles (like "admin", "user", "manager").
This works until:
- You need fine-grained permissions ("can edit their own documents")
- Context matters ("only during business hours")
- Requirements change frequently
ABAC uses attributes instead. Think of it as asking questions:
- "Is this user's department the same as the document's department?"
- "Is the user's clearance level higher than the resource's classification?"
- "Is it currently between 9 AM and 5 PM?"
ABAC follows the XACML architecture with four key components:
- PDP (Policy Decision Point): The engine that evaluates policies
- PIP (Policy Information Point): Attribute providers that fetch attributes
- PAP (Policy Administration Point): Your policy storage (Prisma, files,
etc.)
- PEP (Policy Enforcement Point): Middleware that enforces decisions
This package implements the PDP and provides optional PIP implementations.
``bash`
npm install abac-engine
`typescript
import {
ABACEngine,
PolicyBuilder,
AttributeRef,
CombiningAlgorithm
} from 'abac-engine';
// 1. Create a policy
const policy = PolicyBuilder.create('document-access')
.version('1.0.0')
.permit()
.description('Users can edit their own documents')
.condition(
// Subject's id must equal resource's ownerId
ConditionBuilder.equals(
AttributeRef.subject('id'),
AttributeRef.resource('ownerId')
)
)
.build();
// 2. Create the engine
const engine = new ABACEngine({
combiningAlgorithm: CombiningAlgorithm.DenyOverrides
});
// 3. Make an authorization request
const request = {
subject: {
id: 'user-123',
attributes: { department: 'Engineering' }
},
resource: {
id: 'doc-456',
type: 'document',
attributes: { ownerId: 'user-123', department: 'Engineering' }
},
action: {
id: 'edit'
}
};
// 4. Evaluate
const decision = await engine.evaluate(request, [policy]);
if (decision.decision === Decision.Permit) {
console.log('Access granted!');
} else {
console.log('Access denied');
}
`
A request contains all the information needed to make an authorization decision:
`typescript`
interface ABACRequest {
subject: Subject; // Who is making the request?
resource: Resource; // What are they trying to access?
action: Action; // What are they trying to do?
environment?: Environment; // What's the context?
}
Example:
`typescript`
const request = {
subject: {
id: 'alice',
attributes: {
department: 'Engineering',
role: 'developer',
clearanceLevel: 3
}
},
resource: {
id: 'database-prod',
type: 'database',
attributes: {
environment: 'production',
classification: 2
}
},
action: {
id: 'write'
},
environment: {
currentTime: new Date(),
ipAddress: '192.168.1.100'
}
};
A policy is a rule that grants or denies access based on conditions:
`typescript`
interface ABACPolicy {
id: string; // Unique identifier
version: string; // Policy version
effect: Effect; // Permit or Deny
description?: string; // Human-readable description
condition?: Condition; // When does this policy apply?
obligations?: Obligation[]; // What must happen if policy matches?
advice?: Advice[]; // Optional suggestions
}
Example:
`typescript`
const policy = {
id: 'eng-dept-access',
version: '1.0.0',
effect: Effect.Permit,
description: 'Engineering department members can read engineering documents',
condition: {
operator: LogicalOperator.And,
conditions: [
{
operator: ComparisonOperator.Equals,
left: { category: 'subject', attributeId: 'department' },
right: 'Engineering'
},
{
operator: ComparisonOperator.Equals,
left: { category: 'resource', attributeId: 'department' },
right: 'Engineering'
}
]
}
};
Conditions determine when a policy applies. Three types:
#### Comparison Conditions
Compare two values:
`typescript`
// subject.clearanceLevel > resource.classification
ConditionBuilder.greaterThan(
AttributeRef.subject('clearanceLevel'),
AttributeRef.resource('classification')
);
Available Operators:
- equals, notEqualsgreaterThan
- , greaterThanOrEquallessThan
- , lessThanOrEqualin
- , notIncontains
- , startsWith, endsWithmatchesRegex
- exists
- , notExists
#### Logical Conditions
Combine multiple conditions:
`typescript
// (department === 'Engineering') AND (level > 3)
ConditionBuilder.equals(AttributeRef.subject('department'), 'Engineering').and(
ConditionBuilder.greaterThan(AttributeRef.subject('level'), 3)
);
// (role === 'admin') OR (isOwner === true)
ConditionBuilder.equals(AttributeRef.subject('role'), 'admin').or(
ConditionBuilder.equals(AttributeRef.subject('isOwner'), true)
);
// NOT (status === 'suspended')
ConditionBuilder.equals(AttributeRef.subject('status'), 'suspended').not();
`
#### Function Conditions
Use custom logic:
`typescript
// Register a custom function
engine.registerFunction('is_business_hours', () => {
const hour = new Date().getHours();
return hour >= 9 && hour <= 17;
});
// Use in policy
const policy = PolicyBuilder.create('business-hours-only')
.permit()
.condition(ConditionBuilder.function('is_business_hours'))
.build();
`
When multiple policies apply, how do we decide? That's what combining algorithms
do:
DenyOverrides (recommended)
- If ANY policy denies, the result is Deny
- If at least one permits and none deny, result is Permit
- Use when security is critical
PermitOverrides
- If ANY policy permits, the result is Permit
- If at least one denies and none permit, result is Deny
- Use when availability is more important than security
FirstApplicable
- Use the first policy that matches
- Order matters!
OnlyOneApplicable
- Only one policy should match
- Returns Indeterminate if multiple match
DenyUnlessPermit
- Default to Deny unless explicitly permitted
PermitUnlessDeny
- Default to Permit unless explicitly denied
`typescript`
const engine = new ABACEngine({
combiningAlgorithm: CombiningAlgorithm.DenyOverrides
});
Attribute providers fetch attributes dynamically during evaluation:
`typescript
import {
InMemoryAttributeProvider,
EnvironmentAttributeProvider
} from 'abac-engine';
// In-memory provider for subjects/resources
const subjectProvider = new InMemoryAttributeProvider('subject', 'users');
subjectProvider.setAttribute('user-123', 'department', 'Engineering');
subjectProvider.setAttribute('user-123', 'level', 5);
// Environment provider for context (time, IP, etc.)
const envProvider = new EnvironmentAttributeProvider();
const engine = new ABACEngine({
combiningAlgorithm: CombiningAlgorithm.DenyOverrides,
attributeProviders: [subjectProvider, envProvider]
});
`
Built-in Providers:
- InMemoryAttributeProvider - Store attributes in memoryEnvironmentAttributeProvider
- - Automatic context (time, IP, etc.)DatabaseAttributeProvider
- - Fetch from databaseRestApiAttributeProvider
- - Fetch from REST APILdapAttributeProvider
- - Fetch from LDAPCachedAttributeProvider
- - Add caching to any providerCompositeAttributeProvider
- - Combine multiple providers
You manage policy storage using your preferred method. The engine just evaluates
policies - giving you complete flexibility in how you store and manage them
(PAP).
#### From JSON Files
`typescript
import {
loadPoliciesFromFile,
loadAndValidatePoliciesFromFile
} from 'abac-engine';
// Basic load
const policies = await loadPoliciesFromFile('./policies.json');
// Load with automatic validation
const { policies, validationResults } =
await loadAndValidatePoliciesFromFile('./policies.json');
// Throws ValidationError if any policy is invalid
`
#### From JSON Strings
`typescript
import { loadPoliciesFromJSON } from 'abac-engine';
const jsonString =
'[{"id": "policy-1", "version": "1.0.0", "effect": "Permit", ...}]';
const policies = loadPoliciesFromJSON(jsonString);
`
#### To JSON Files
`typescript
import {
savePolicyToFile,
savePoliciesToFile,
saveAndValidatePolicyToFile,
saveAndValidatePoliciesToFile
} from 'abac-engine';
// Save a single policy
const policy = PolicyBuilder.create('my-policy')
.permit()
.condition(
ConditionBuilder.equals(Attributes.subject.id, Attributes.resource.owner)
)
.build();
await savePolicyToFile(policy, './policies/my-policy.json');
// Save multiple policies
const policies = [policy1, policy2, policy3];
await savePoliciesToFile(policies, './policies/all-policies.json');
// Save with automatic validation (throws if invalid)
await saveAndValidatePolicyToFile(policy, './policies/validated-policy.json');
await saveAndValidatePoliciesToFile(
policies,
'./policies/validated-policies.json'
);
`
#### Export to JSON Strings
`typescript
import { exportPolicyToJSON, exportPoliciesToJSON } from 'abac-engine';
// Export single policy (pretty-printed by default)
const policyJson = exportPolicyToJSON(policy);
console.log(policyJson);
// Export without pretty-printing (compact)
const compactJson = exportPolicyToJSON(policy, false);
// Export multiple policies
const policiesJson = exportPoliciesToJSON([policy1, policy2, policy3]);
`
`typescript
// schema.prisma
model AbacPolicy {
id String @id
version String
effect String
description String?
condition Json?
createdAt DateTime @default(now())
}
// Your code
import { validatePolicy } from 'abac-engine';
// Load policies
const policies = await prisma.abacPolicy.findMany();
// Save with validation
async function savePolicy(policy: ABACPolicy) {
const validation = validatePolicy(policy);
if (!validation.valid) {
throw new Error(validation.errors.map(e => e.message).join(', '));
}
await prisma.abacPolicy.create({ data: policy });
}
// Load policies from database
const dbPolicies = await prisma.abacPolicy.findMany();
// Evaluate
const decision = await engine.evaluate(request, dbPolicies);
`
#### Version Control for Policies
`typescript
import {
savePoliciesToFile,
loadAndValidatePoliciesFromFile
} from 'abac-engine';
// Save policies to version-controlled file
const policies = [
PolicyPatterns.ownership(['read', 'update']),
PolicyPatterns.departmentAccess(['read'], ['public', 'internal'])
];
await savePoliciesToFile(policies, './config/policies.json');
// Commit to git for versioning and review
`
#### Hot Reload Policies
`typescript
import { loadPoliciesFromFile } from 'abac-engine';
import { watch } from 'fs';
let currentPolicies: ABACPolicy[] = [];
async function reloadPolicies() {
currentPolicies = await loadPoliciesFromFile('./policies.json');
console.log(Loaded ${currentPolicies.length} policies);
}
// Initial load
await reloadPolicies();
// Watch for changes
watch('./policies.json', async () => {
await reloadPolicies();
});
`
#### Migration: Export from Database to Files
`typescript
import { savePoliciesToFile } from 'abac-engine';
// Export policies from database to file system
async function exportPolicies() {
const policies = await prisma.abacPolicy.findMany();
await savePoliciesToFile(policies, './backup/policies.json', true);
console.log(Exported ${policies.length} policies);
}
await exportPolicies();
`
#### Import Policies into Database
`typescript
import { loadAndValidatePoliciesFromFile } from 'abac-engine';
async function importPolicies() {
const { policies } = await loadAndValidatePoliciesFromFile('./policies.json');
for (const policy of policies) {
await prisma.abacPolicy.upsert({
where: { id: policy.id },
update: policy,
create: policy
});
}
console.log(Imported ${policies.length} policies);
}
await importPolicies();
`
`typescript
import { PolicyCache } from 'abac-engine';
const cache = new PolicyCache(300); // 5 minutes TTL
async function getPolicies() {
return await cache.get(async () => {
return await prisma.abacPolicy.findMany();
});
}
const policies = await getPolicies(); // Loads from DB
const policies2 = await getPolicies(); // Uses cache
// Invalidate when policies change
cache.invalidate();
// Example: Cache with file-based policies
const fileCache = new PolicyCache(60); // 1 minute TTL
async function getCachedPolicies() {
return await fileCache.get(async () => {
return await loadPoliciesFromFile('./policies.json');
});
}
`
`typescript
const policies = [
// Owners can do anything with their documents
PolicyBuilder.create('owner-full-access')
.permit()
.description('Document owners have full access')
.condition(
ConditionBuilder.equals(
AttributeRef.subject('id'),
AttributeRef.resource('ownerId')
)
)
.build(),
// Same department members can read
PolicyBuilder.create('dept-read-access')
.permit()
.description('Department members can read department documents')
.condition(
ConditionBuilder.equals(
AttributeRef.subject('department'),
AttributeRef.resource('department')
).and(ConditionBuilder.equals(AttributeRef.action('id'), 'read'))
)
.build(),
// Admins can do everything
PolicyBuilder.create('admin-access')
.permit()
.description('Admins have full access')
.condition(ConditionBuilder.equals(AttributeRef.subject('role'), 'admin'))
.build()
];
`
`typescript
// Tenant isolation policy
const tenantIsolation = PolicyBuilder.create('tenant-isolation')
.deny()
.description('Users cannot access other tenants resources')
.condition(
ConditionBuilder.notEquals(
AttributeRef.subject('tenantId'),
AttributeRef.resource('tenantId')
)
)
.build();
// Usage
const request = {
subject: {
id: 'user-1',
attributes: { tenantId: 'tenant-a' }
},
resource: {
id: 'resource-1',
type: 'data',
attributes: { tenantId: 'tenant-b' } // Different tenant!
},
action: { id: 'read' }
};
const decision = await engine.evaluate(request, [tenantIsolation]);
// Result: Deny
`
`typescript
// HIPAA-compliant access control
const policies = [
// Doctors can access their patients' records
PolicyBuilder.create('doctor-patient-access')
.permit()
.condition(
ConditionBuilder.equals(AttributeRef.subject('role'), 'doctor').and(
ConditionBuilder.in(
AttributeRef.resource('patientId'),
AttributeRef.subject('assignedPatients')
)
)
)
.build(),
// Emergency access (break-glass)
PolicyBuilder.create('emergency-access')
.permit()
.description('Emergency access with audit logging')
.condition(
ConditionBuilder.equals(AttributeRef.subject('emergencyMode'), true)
)
.logObligation({
level: 'critical',
message: 'Emergency access used',
timestamp: new Date()
})
.build()
];
`
`typescript
// Register custom time function
engine.registerFunction('is_business_hours', () => {
const hour = new Date().getHours();
const day = new Date().getDay();
return day >= 1 && day <= 5 && hour >= 9 && hour <= 17;
});
const policy = PolicyBuilder.create('business-hours-only')
.permit()
.description('Certain operations only allowed during business hours')
.condition(
ConditionBuilder.function('is_business_hours').and(
ConditionBuilder.equals(AttributeRef.action('id'), 'deploy')
)
)
.build();
`
`typescript
class ABACEngine {
constructor(config: ABACEngineConfig);
evaluate(request: ABACRequest, policies: ABACPolicy[]): Promise
registerFunction(name: string, fn: ConditionFunction): void;
getMetrics(): EvaluationMetrics;
getAuditLog(): ABACAccessLog[];
}
`
`typescript`
PolicyBuilder.create(id: string)
.version(version: string)
.permit() | .deny()
.description(description: string)
.condition(condition: Condition | ConditionBuilder)
.target(target: PolicyTarget | TargetBuilder)
.logObligation(params: Record
.notifyObligation(params: Record
.build(): ABACPolicy
`typescript
// Comparison
ConditionBuilder.equals(left, right);
ConditionBuilder.notEquals(left, right);
ConditionBuilder.greaterThan(left, right);
ConditionBuilder.lessThan(left, right);
ConditionBuilder.in(value, array);
ConditionBuilder.contains(haystack, needle);
ConditionBuilder.exists(attribute);
// Logical
condition.and(otherCondition);
condition.or(otherCondition);
condition.not();
// Function
ConditionBuilder.function(name, ...args);
`
`typescript`
AttributeRef.subject(attributeId: string)
AttributeRef.resource(attributeId: string)
AttributeRef.action(attributeId: string)
AttributeRef.environment(attributeId: string)
`typescript
import { validatePolicy, validatePolicies } from 'abac-engine';
// Validate single policy
const result = validatePolicy(policy);
if (!result.valid) {
console.error(result.errors);
}
// Validate multiple
const results = validatePolicies(policies);
// Validate and throw
validatePolicyOrThrow(policy); // Throws if invalid
`
Obligations are actions that MUST happen if a policy matches:
`typescript`
const policy = PolicyBuilder.create('audit-sensitive-access')
.permit()
.condition(...)
.logObligation({
level: 'warning',
message: 'Sensitive data accessed',
userId: AttributeRef.subject('id')
})
.notifyObligation({
recipient: 'security@company.com',
subject: 'Sensitive Access Alert'
})
.build();
Advice are optional suggestions:
`typescript`
const policy = PolicyBuilder.create('risky-operation')
.permit()
.condition(...)
.advice([{
id: 'mfa-recommendation',
type: 'custom',
parameters: {
message: 'Consider requiring MFA for this operation'
}
}])
.build();
`typescript
import { filterPoliciesByTarget, groupPoliciesByEffect } from 'abac-engine';
// Pre-filter policies before evaluation
const relevantPolicies = filterPoliciesByTarget(allPolicies, {
resourceType: 'document',
actionId: 'read'
});
// Group by effect for faster processing
const { permit, deny } = groupPoliciesByEffect(policies);
// Use caching
const cache = new PolicyCache(300);
`
`typescript
class CustomDatabaseProvider extends BaseAttributeProvider {
constructor(private db: Database) {
super('subject', 'custom-users');
}
async getAttributes(id: string): Promise
const user = await this.db.users.findOne({ id });
return {
department: user.department,
role: user.role,
permissions: user.permissions
};
}
supportsAttribute(attributeId: string): boolean {
return ['department', 'role', 'permissions'].includes(attributeId);
}
}
`
The ABAC Engine supports pluggable logging for debugging and monitoring. By
default, it uses a SilentLogger that doesn't output anything, making it
production-safe without configuration.
#### Using the Default Console Logger
`typescript
import { ABACEngine, ConsoleLogger, LogLevel } from 'abac-engine';
const engine = new ABACEngine({
combiningAlgorithm: CombiningAlgorithm.DenyOverrides,
logger: new ConsoleLogger(LogLevel.Warn) // Only log warnings and errors
});
`
Available Log Levels:
- LogLevel.Debug - All messagesLogLevel.Info
- - Info, warnings, and errorsLogLevel.Warn
- - Warnings and errors onlyLogLevel.Error
- - Errors onlyLogLevel.None
- - No logging
#### Using a Custom Logger
Integrate with your existing logging solution (Winston, Pino, Bunyan, etc.):
`typescript
import { ILogger } from 'abac-engine';
import winston from 'winston';
class WinstonLogger implements ILogger {
private logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [new winston.transports.File({ filename: 'abac.log' })]
});
debug(message: string, meta?: Record
this.logger.debug(message, meta);
}
info(message: string, meta?: Record
this.logger.info(message, meta);
}
warn(message: string, meta?: Record
this.logger.warn(message, meta);
}
error(
message: string,
error?: Error | unknown,
meta?: Record
): void {
this.logger.error(message, { error, ...meta });
}
}
const engine = new ABACEngine({
combiningAlgorithm: CombiningAlgorithm.DenyOverrides,
logger: new WinstonLogger()
});
`
#### Logging in Attribute Providers
Attribute providers also support logging:
`typescript
import {
InMemoryAttributeProvider,
ConsoleLogger,
LogLevel
} from 'abac-engine';
const logger = new ConsoleLogger(LogLevel.Debug);
const subjectProvider = new InMemoryAttributeProvider(
'subject',
'users',
{ user123: { department: 'Engineering' } },
logger // Pass the same logger for consistency
);
const engine = new ABACEngine({
combiningAlgorithm: CombiningAlgorithm.DenyOverrides,
attributeProviders: [subjectProvider],
logger // Use the same logger instance across components
});
`
What Gets Logged:
- Attribute provider errors (failed database queries, API calls, etc.)
- Policy applicability errors
- Evaluation warnings and errors
Testing is simple - just pass arrays of policies:
`typescript
import { ABACEngine, PolicyBuilder, Decision } from 'abac-engine';
describe('Authorization', () => {
it('should allow owners to edit', async () => {
const policy = PolicyBuilder.create('owner-edit')
.permit()
.condition(
ConditionBuilder.equals(
AttributeRef.subject('id'),
AttributeRef.resource('ownerId')
)
)
.build();
const engine = new ABACEngine({
combiningAlgorithm: CombiningAlgorithm.DenyOverrides
});
const request = {
subject: { id: 'alice', attributes: {} },
resource: {
id: 'doc',
type: 'document',
attributes: { ownerId: 'alice' }
},
action: { id: 'edit' }
};
const decision = await engine.evaluate(request, [policy]);
expect(decision.decision).toBe(Decision.Permit);
});
});
`
- ABAC: Attribute-Based Access Control - authorization based on attributes
- Attribute: A property of a subject, resource, action, or environment
- Combining Algorithm: How to resolve conflicts when multiple policies apply
- Condition: Boolean expression that determines if a policy applies
- Decision: Result of evaluation (Permit, Deny, NotApplicable,
Indeterminate)
- Effect: What a policy does if it matches (Permit or Deny)
- Obligation: Action that MUST happen if a policy matches
- Advice: Optional suggestion from a policy
- PAP: Policy Administration Point - where policies are managed (your
database)
- PDP: Policy Decision Point - the evaluation engine (this library)
- PEP: Policy Enforcement Point - enforces decisions (your middleware)
- PIP: Policy Information Point - provides attributes (attribute providers)
- Policy: A rule that grants or denies access
- Request: The authorization question being asked
- Subject: Who is requesting access (user, service, etc.)
- Resource: What is being accessed (document, API, database, etc.)
- Action: What operation is being performed (read, write, delete, etc.)
- Target: Optional filter to determine if policy applies
Fully typed with TypeScript:
`typescript`
import type {
ABACPolicy,
ABACRequest,
ABACDecision,
Condition,
Effect,
Decision
} from 'abac-engine';
- Zero dependencies
- Synchronous condition evaluation where possible
- Built-in caching support
- Efficient policy matching
- Benchmarks: ~10,000 evaluations/second (simple policies)
Complete documentation is available in the /docs directory:
- Documentation Index - Complete documentation
navigation and quick reference
- API Reference - Full API documentation for all
classes and methods
- Glossary - Comprehensive guide to all ABAC terms and
concepts
- Policy Guide - How to write effective ABAC
policies
- Examples - Real-world use cases and integration
examples
- Error Handling - Error handling best practices
Need clarification on specific terms?
- What is ABAC? -
Understanding Attribute-Based Access Control
- What is a Tenant? - Multi-tenancy explained
- What is Multi-Tenant? - Isolating
customers in SaaS
- PDP, PIP, PAP, PEP? - ABAC
architecture components
- Combining Algorithms? - How to
resolve policy conflicts
- Obligations vs Advice? - Required vs
optional actions
- Document Management System -
Complete DMS example
- Multi-Tenant SaaS -
Tenant isolation
- Healthcare System -
HIPAA-compliant access
- Financial Services - Banking
access control
- Express.js Integration - REST
API middleware
- NestJS Integration - Guard
implementation
MIT
Issues and PRs welcome on GitHub!
- ABAC Guide (NIST)
- XACML Standard
- Code Examples
- Complete Documentation
---
Ready to get started? Install now: npm install abac-engine`
Need help? Check the Glossary for terminology or
Examples for real-world use cases.