Error Handling
Handle errors gracefully in your ABAC implementation.
Overview
Understanding error handling in abac-engine.
The ABAC engine uses a fail-safe approach to error handling. When errors occur during policy evaluation, the engine defaults to denying access to ensure security is maintained.
Key Principles
- Fail-Safe: Errors default to Deny to maintain security
- Graceful Degradation: Continue evaluation when possible
- Detailed Logging: Capture error context for debugging
- Type Safety: TypeScript helps prevent common errors
Error Types
Common error types you may encounter.
Validation Errors
Occur when policy structure or request data is invalid.
import { ABACEngine } from 'abac-engine';
try {
const policy = {
id: 'invalid-policy',
version: '1.0.0',
effect: 'InvalidEffect', // Invalid effect
condition: null
};
const engine = new ABACEngine({ policies: [policy] });
} catch (error) {
if (error instanceof ValidationError) {
console.error('Policy validation failed:', error.message);
console.error('Validation errors:', error.errors);
}
}
// Common validation errors:
// - Invalid effect (must be 'Permit' or 'Deny')
// - Missing required fields (id, version, effect)
// - Invalid condition structure
// - Invalid operator in condition
// - Malformed attribute referencesAttribute Resolution Errors
Occur when attributes cannot be found or retrieved.
// Missing Attributes
const decision = await engine.evaluate({
subject: { id: 'user-123' }, // Missing 'department' attribute
resource: { id: 'doc-456' },
action: { id: 'read' },
environment: {}
});
// The engine will:
// 1. Log a warning about the missing attribute
// 2. Treat the missing attribute as undefined
// 3. Continue evaluation (may result in Deny if condition requires the attribute)
// Attribute Provider Errors
const provider = {
name: 'user-provider',
async provide(category, id, attributes) {
throw new Error('Database connection failed');
}
};
const engine = new ABACEngine({
policies,
attributeProviders: [provider]
});
try {
const decision = await engine.evaluate(request);
} catch (error) {
console.error('Attribute provider error:', error);
// Engine defaults to Deny for safety
}Condition Evaluation Errors
Occur during condition evaluation, such as type mismatches or invalid operations.
// Type Mismatch
const condition = ConditionBuilder.greaterThan(
AttributeRef.subject('name'), // String value
100 // Numeric comparison
);
// Custom Function Errors
const policy = PolicyBuilder
.create('custom-function-policy')
.permit()
.condition({
type: 'function',
function: (context) => {
throw new Error('Unexpected error in custom function');
}
})
.build();
// Handling condition errors
const engine = new ABACEngine({
policies,
onConditionError: (error, policy, context) => {
console.error(`Error evaluating policy ${policy.id}:`, error);
// Return false to deny access, or throw to halt evaluation
return false;
}
});Timeout Errors
Occur when evaluation takes too long.
const engine = new ABACEngine({
policies,
maxEvaluationTime: 5000 // 5 seconds
});
try {
const decision = await engine.evaluate(request);
} catch (error) {
if (error instanceof TimeoutError) {
console.error('Policy evaluation timed out');
// Handle timeout - typically deny access
}
}Error Handling Patterns
Recommended patterns for handling errors in your application.
Try-Catch Pattern
async function checkAccess(request) {
try {
const decision = await engine.evaluate(request);
if (decision.decision === 'Permit') {
return { allowed: true };
} else {
return {
allowed: false,
reason: decision.reason || 'Access denied by policy'
};
}
} catch (error) {
// Log the error for debugging
console.error('ABAC evaluation error:', error);
// Return a safe default (deny access)
return {
allowed: false,
reason: 'Access denied due to evaluation error',
error: error.message
};
}
}Middleware Error Handling
function createABACMiddleware(engine) {
return async (req, res, next) => {
try {
const decision = await engine.evaluate({
subject: req.user,
resource: {
type: req.baseUrl,
id: req.params.id
},
action: { id: req.method.toLowerCase() },
environment: {
ip: req.ip,
timestamp: new Date().toISOString()
}
});
if (decision.decision === 'Permit') {
// Attach decision to request for logging
req.abacDecision = decision;
next();
} else {
res.status(403).json({
error: 'Access Denied',
reason: decision.reason
});
}
} catch (error) {
// Log error with full context
console.error('ABAC middleware error:', {
error: error.message,
stack: error.stack,
user: req.user?.id,
resource: req.params.id,
action: req.method
});
// Return 403 (not 500) to avoid leaking system details
res.status(403).json({
error: 'Access Denied',
message: 'Unable to evaluate access'
});
}
};
}Comprehensive Logging
import { ILogger } from 'abac-engine';
class CustomLogger implements ILogger {
info(message: string, meta?: any): void {
console.log(`[INFO] ${message}`, meta);
}
warn(message: string, meta?: any): void {
console.warn(`[WARN] ${message}`, meta);
}
error(message: string, meta?: any): void {
console.error(`[ERROR] ${message}`, meta);
// Send to error tracking service
sendToSentry({ message, meta });
}
debug(message: string, meta?: any): void {
if (process.env.NODE_ENV === 'development') {
console.debug(`[DEBUG] ${message}`, meta);
}
}
}
const engine = new ABACEngine({
policies,
logger: new CustomLogger()
});
// The engine will log:
// - Policy evaluation start/end
// - Attribute resolution issues
// - Condition evaluation errors
// - Performance metrics
// - Final decisionsFallback Strategies
class ResilientABACEngine {
constructor(
private primaryEngine: ABACEngine,
private fallbackEngine?: ABACEngine
) {}
async evaluate(request: any) {
try {
return await this.primaryEngine.evaluate(request);
} catch (error) {
console.error('Primary engine failed:', error);
if (this.fallbackEngine) {
try {
console.log('Attempting fallback engine...');
return await this.fallbackEngine.evaluate(request);
} catch (fallbackError) {
console.error('Fallback engine also failed:', fallbackError);
}
}
// Ultimate fallback: deny access
return {
decision: 'Deny',
reason: 'System error occurred during evaluation',
policies: [],
obligations: []
};
}
}
}
// Usage
const resilientEngine = new ResilientABACEngine(
primaryEngine,
fallbackEngine
);Best Practices
Always Use Try-Catch
Wrap all policy evaluation calls in try-catch blocks to handle unexpected errors gracefully.
Default to Deny
When errors occur, always default to denying access. Security should be the priority.
Log with Context
Include user ID, resource ID, action, and other context in error logs for easier debugging.
Validate Policies
Validate policy structure before loading them into the engine. Use schema validation.
Monitor Error Rates
Track error rates and set up alerts. Sudden spikes may indicate policy misconfigurations.
Test Error Scenarios
Write tests that simulate errors: missing attributes, provider failures, invalid data.
Don't Expose Internal Errors
Don't return internal error details to end users. Use generic messages like “Access Denied”.
Don't Ignore Warnings
Pay attention to warning logs. They often indicate configuration issues that need addressing.
Testing Error Handling
import { describe, test, expect } from 'vitest';
import { ABACEngine } from 'abac-engine';
describe('Error Handling', () => {
test('handles missing attributes gracefully', async () => {
const policy = PolicyBuilder
.create('test-policy')
.permit()
.condition(
ConditionBuilder.equals(
AttributeRef.subject('department'),
AttributeRef.resource('department')
)
)
.build();
const engine = new ABACEngine({ policies: [policy] });
// Request missing department attributes
const decision = await engine.evaluate({
subject: { id: 'user-123' }, // Missing department
resource: { id: 'doc-456' }, // Missing department
action: { id: 'read' },
environment: {}
});
expect(decision.decision).toBe('Deny');
});
test('handles attribute provider errors', async () => {
const failingProvider = {
name: 'failing-provider',
async provide() {
throw new Error('Provider error');
}
};
const engine = new ABACEngine({
policies: [policy],
attributeProviders: [failingProvider]
});
await expect(async () => {
await engine.evaluate(request);
}).rejects.toThrow();
});
test('handles invalid condition operators', () => {
const invalidPolicy = {
id: 'invalid',
version: '1.0.0',
effect: 'Permit',
condition: {
operator: 'nonexistent',
left: { category: 'subject', attributeId: 'id' },
right: 'value'
}
};
expect(() => {
new ABACEngine({ policies: [invalidPolicy] });
}).toThrow();
});
test('handles timeout errors', async () => {
const slowPolicy = PolicyBuilder
.create('slow-policy')
.permit()
.condition({
type: 'function',
function: async () => {
await new Promise(resolve => setTimeout(resolve, 10000));
return true;
}
})
.build();
const engine = new ABACEngine({
policies: [slowPolicy],
maxEvaluationTime: 100 // 100ms timeout
});
await expect(async () => {
await engine.evaluate(request);
}).rejects.toThrow('timeout');
});
});