Error Handling

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 references

Attribute 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 decisions

Fallback 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');
  });
});

Next Steps