Policy Persistence

Policy Persistence

Store and manage ABAC policies using various persistence mechanisms.

Overview

abac-engine is designed to be flexible with policy storage and retrieval.

While the ABAC engine evaluates policies in-memory for optimal performance, you need a persistence layer to store and manage policies long-term. The engine is agnostic to how you store policies - you can use files, databases, or any custom solution.

Key Concepts

  • In-Memory Evaluation: Policies are evaluated from memory for speed
  • Flexible Persistence: Store policies however you prefer
  • Dynamic Loading: Load and reload policies at runtime
  • Versioning: Track policy changes over time

File-Based Persistence

Store policies in JSON files for simple deployment and version control.

Single Policy File

Store all policies in a single JSON file.

// policies.json
{
  "policies": [
    {
      "id": "owner-access",
      "version": "1.0.0",
      "effect": "Permit",
      "description": "Owners can access their resources",
      "condition": {
        "operator": "equals",
        "left": { "category": "subject", "attributeId": "id" },
        "right": { "category": "resource", "attributeId": "ownerId" }
      }
    },
    {
      "id": "department-access",
      "version": "1.0.0",
      "effect": "Permit",
      "description": "Department members can read documents",
      "condition": {
        "operator": "and",
        "conditions": [
          {
            "operator": "equals",
            "left": { "category": "subject", "attributeId": "department" },
            "right": { "category": "resource", "attributeId": "department" }
          },
          {
            "operator": "in",
            "left": { "category": "action", "attributeId": "id" },
            "right": ["read", "view"]
          }
        ]
      }
    }
  ]
}
// load-policies.ts
import fs from 'fs/promises';
import { ABACEngine, ABACPolicy } from 'abac-engine';

async function loadPoliciesFromFile(filePath: string): Promise<ABACPolicy[]> {
  const content = await fs.readFile(filePath, 'utf-8');
  const data = JSON.parse(content);
  return data.policies;
}

async function createEngine() {
  const policies = await loadPoliciesFromFile('./policies.json');
  return new ABACEngine({ policies });
}

// Usage
const engine = await createEngine();

Multiple Policy Files

Organize policies into separate files by domain or category.

// File structure
policies/
  ├── documents/
  │   ├── owner-access.json
  │   ├── department-read.json
  │   └── confidential.json
  ├── users/
  │   ├── profile-access.json
  │   └── admin-access.json
  └── billing/
      └── billing-admin.json

// load-policies.ts
import fs from 'fs/promises';
import path from 'path';
import { ABACPolicy } from 'abac-engine';

async function loadPoliciesFromDirectory(dirPath: string): Promise<ABACPolicy[]> {
  const policies: ABACPolicy[] = [];

  async function readDirectory(dir: string) {
    const entries = await fs.readdir(dir, { withFileTypes: true });

    for (const entry of entries) {
      const fullPath = path.join(dir, entry.name);

      if (entry.isDirectory()) {
        await readDirectory(fullPath);
      } else if (entry.name.endsWith('.json')) {
        const content = await fs.readFile(fullPath, 'utf-8');
        const policy = JSON.parse(content);
        policies.push(policy);
      }
    }
  }

  await readDirectory(dirPath);
  return policies;
}

// Usage
const policies = await loadPoliciesFromDirectory('./policies');
const engine = new ABACEngine({ policies });

YAML Format

Use YAML for more readable policy definitions.

# owner-access.yaml
id: owner-access
version: 1.0.0
effect: Permit
description: Owners can access their resources
condition:
  operator: equals
  left:
    category: subject
    attributeId: id
  right:
    category: resource
    attributeId: ownerId

# load-yaml-policies.ts
import fs from 'fs/promises';
import yaml from 'yaml';
import { ABACPolicy } from 'abac-engine';

async function loadPolicyFromYAML(filePath: string): Promise<ABACPolicy> {
  const content = await fs.readFile(filePath, 'utf-8');
  return yaml.parse(content);
}

const policy = await loadPolicyFromYAML('./owner-access.yaml');

Database Persistence

Store policies in a database for dynamic management and versioning.

SQL Database (PostgreSQL, MySQL)

-- Schema
CREATE TABLE policies (
  id VARCHAR(255) PRIMARY KEY,
  version VARCHAR(50) NOT NULL,
  effect VARCHAR(10) NOT NULL CHECK (effect IN ('Permit', 'Deny')),
  description TEXT,
  condition JSONB NOT NULL,
  target JSONB,
  priority INTEGER DEFAULT 0,
  obligations JSONB,
  advice JSONB,
  is_active BOOLEAN DEFAULT true,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  created_by VARCHAR(255),
  metadata JSONB
);

CREATE INDEX idx_policies_active ON policies(is_active);
CREATE INDEX idx_policies_priority ON policies(priority DESC);

-- Version history table
CREATE TABLE policy_versions (
  id SERIAL PRIMARY KEY,
  policy_id VARCHAR(255) NOT NULL,
  version VARCHAR(50) NOT NULL,
  data JSONB NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  created_by VARCHAR(255),
  FOREIGN KEY (policy_id) REFERENCES policies(id) ON DELETE CASCADE
);
// policy-repository.ts
import { Pool } from 'pg';
import { ABACPolicy } from 'abac-engine';

export class PolicyRepository {
  constructor(private pool: Pool) {}

  async loadPolicies(): Promise<ABACPolicy[]> {
    const result = await this.pool.query(
      'SELECT * FROM policies WHERE is_active = true ORDER BY priority DESC'
    );

    return result.rows.map(row => ({
      id: row.id,
      version: row.version,
      effect: row.effect,
      description: row.description,
      condition: row.condition,
      target: row.target,
      priority: row.priority,
      obligations: row.obligations,
      advice: row.advice,
      metadata: row.metadata
    }));
  }

  async savePolicy(policy: ABACPolicy, userId: string): Promise<void> {
    await this.pool.query(
      `INSERT INTO policies
       (id, version, effect, description, condition, target, priority,
        obligations, advice, created_by, metadata)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
       ON CONFLICT (id) DO UPDATE SET
         version = EXCLUDED.version,
         effect = EXCLUDED.effect,
         description = EXCLUDED.description,
         condition = EXCLUDED.condition,
         target = EXCLUDED.target,
         priority = EXCLUDED.priority,
         obligations = EXCLUDED.obligations,
         advice = EXCLUDED.advice,
         updated_at = CURRENT_TIMESTAMP`,
      [
        policy.id,
        policy.version,
        policy.effect,
        policy.description,
        JSON.stringify(policy.condition),
        JSON.stringify(policy.target),
        policy.priority || 0,
        JSON.stringify(policy.obligations),
        JSON.stringify(policy.advice),
        userId,
        JSON.stringify(policy.metadata)
      ]
    );

    // Save version history
    await this.pool.query(
      `INSERT INTO policy_versions (policy_id, version, data, created_by)
       VALUES ($1, $2, $3, $4)`,
      [policy.id, policy.version, JSON.stringify(policy), userId]
    );
  }

  async deletePolicy(policyId: string): Promise<void> {
    await this.pool.query('DELETE FROM policies WHERE id = $1', [policyId]);
  }

  async getPolicyVersions(policyId: string): Promise<ABACPolicy[]> {
    const result = await this.pool.query(
      'SELECT data FROM policy_versions WHERE policy_id = $1 ORDER BY created_at DESC',
      [policyId]
    );

    return result.rows.map(row => row.data);
  }
}

// Usage
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const repository = new PolicyRepository(pool);

const policies = await repository.loadPolicies();
const engine = new ABACEngine({ policies });

MongoDB / NoSQL

// policy-service.ts
import { MongoClient, Collection } from 'mongodb';
import { ABACPolicy } from 'abac-engine';

export class PolicyService {
  private collection: Collection<ABACPolicy>;

  constructor(client: MongoClient) {
    const db = client.db('abac');
    this.collection = db.collection<ABACPolicy>('policies');

    // Create indexes
    this.collection.createIndex({ isActive: 1, priority: -1 });
    this.collection.createIndex({ 'metadata.category': 1 });
  }

  async loadActivePolicies(): Promise<ABACPolicy[]> {
    return await this.collection
      .find({ isActive: true })
      .sort({ priority: -1 })
      .toArray();
  }

  async savePolicy(policy: ABACPolicy): Promise<void> {
    await this.collection.updateOne(
      { id: policy.id },
      { $set: policy },
      { upsert: true }
    );
  }

  async deletePolicy(policyId: string): Promise<void> {
    await this.collection.deleteOne({ id: policyId });
  }

  async findPoliciesByCategory(category: string): Promise<ABACPolicy[]> {
    return await this.collection
      .find({ 'metadata.category': category, isActive: true })
      .toArray();
  }
}

// Usage
const client = new MongoClient(process.env.MONGODB_URI);
await client.connect();

const service = new PolicyService(client);
const policies = await service.loadActivePolicies();
const engine = new ABACEngine({ policies });

Using Prisma ORM

// prisma/schema.prisma
model Policy {
  id          String   @id
  version     String
  effect      String
  description String?
  condition   Json
  target      Json?
  priority    Int      @default(0)
  obligations Json?
  advice      Json?
  isActive    Boolean  @default(true)
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
  createdBy   String?
  metadata    Json?

  versions    PolicyVersion[]

  @@index([isActive, priority])
}

model PolicyVersion {
  id        Int      @id @default(autoincrement())
  policyId  String
  version   String
  data      Json
  createdAt DateTime @default(now())
  createdBy String?

  policy    Policy   @relation(fields: [policyId], references: [id], onDelete: Cascade)

  @@index([policyId])
}

// policy-service.ts
import { PrismaClient } from '@prisma/client';
import { ABACPolicy } from 'abac-engine';

const prisma = new PrismaClient();

export async function loadPolicies(): Promise<ABACPolicy[]> {
  const policies = await prisma.policy.findMany({
    where: { isActive: true },
    orderBy: { priority: 'desc' }
  });

  return policies.map(p => ({
    id: p.id,
    version: p.version,
    effect: p.effect as 'Permit' | 'Deny',
    description: p.description || undefined,
    condition: p.condition as any,
    target: p.target as any,
    priority: p.priority,
    obligations: p.obligations as any,
    advice: p.advice as any,
    metadata: p.metadata as any
  }));
}

export async function savePolicy(policy: ABACPolicy, userId: string): Promise<void> {
  await prisma.$transaction(async (tx) => {
    await tx.policy.upsert({
      where: { id: policy.id },
      update: {
        version: policy.version,
        effect: policy.effect,
        description: policy.description,
        condition: policy.condition as any,
        target: policy.target as any,
        priority: policy.priority || 0,
        obligations: policy.obligations as any,
        advice: policy.advice as any,
        metadata: policy.metadata as any,
        updatedAt: new Date()
      },
      create: {
        id: policy.id,
        version: policy.version,
        effect: policy.effect,
        description: policy.description,
        condition: policy.condition as any,
        target: policy.target as any,
        priority: policy.priority || 0,
        obligations: policy.obligations as any,
        advice: policy.advice as any,
        createdBy: userId,
        metadata: policy.metadata as any
      }
    });

    await tx.policyVersion.create({
      data: {
        policyId: policy.id,
        version: policy.version,
        data: policy as any,
        createdBy: userId
      }
    });
  });
}

// Usage
const policies = await loadPolicies();
const engine = new ABACEngine({ policies });

Dynamic Policy Loading

Load and reload policies at runtime without restarting your application.

Hot Reload Policies

Automatically reload policies when they change in the database or file system.

import { ABACEngine, ABACPolicy } from 'abac-engine';

export class PolicyManager {
  private engine: ABACEngine;
  private reloadInterval: NodeJS.Timeout | null = null;

  constructor(
    private loadPolicies: () => Promise<ABACPolicy[]>,
    private reloadIntervalMs: number = 60000 // 1 minute
  ) {
    this.engine = new ABACEngine({ policies: [] });
  }

  async initialize(): Promise<void> {
    await this.reload();
    this.startAutoReload();
  }

  async reload(): Promise<void> {
    const policies = await this.loadPolicies();
    this.engine = new ABACEngine({ policies });
    console.log(`Loaded ${policies.length} policies`);
  }

  startAutoReload(): void {
    if (this.reloadInterval) {
      clearInterval(this.reloadInterval);
    }

    this.reloadInterval = setInterval(async () => {
      try {
        await this.reload();
      } catch (error) {
        console.error('Failed to reload policies:', error);
      }
    }, this.reloadIntervalMs);
  }

  stopAutoReload(): void {
    if (this.reloadInterval) {
      clearInterval(this.reloadInterval);
      this.reloadInterval = null;
    }
  }

  getEngine(): ABACEngine {
    return this.engine;
  }

  async evaluate(request: any) {
    return this.engine.evaluate(request);
  }
}

// Usage
const manager = new PolicyManager(
  async () => await repository.loadPolicies(),
  60000 // Reload every minute
);

await manager.initialize();

// Use the manager for evaluation
const decision = await manager.evaluate(request);

// Manual reload
await manager.reload();

Policy Cache with TTL

Cache policies with automatic expiration and refresh.

import { ABACEngine, ABACPolicy } from 'abac-engine';

export class CachedPolicyLoader {
  private engine: ABACEngine | null = null;
  private lastLoad: number = 0;

  constructor(
    private loadPolicies: () => Promise<ABACPolicy[]>,
    private ttlMs: number = 300000 // 5 minutes
  ) {}

  async getEngine(): Promise<ABACEngine> {
    const now = Date.now();

    if (!this.engine || now - this.lastLoad > this.ttlMs) {
      const policies = await this.loadPolicies();
      this.engine = new ABACEngine({ policies });
      this.lastLoad = now;
    }

    return this.engine;
  }

  async evaluate(request: any) {
    const engine = await this.getEngine();
    return engine.evaluate(request);
  }

  invalidate(): void {
    this.engine = null;
    this.lastLoad = 0;
  }
}

// Usage
const loader = new CachedPolicyLoader(
  async () => await repository.loadPolicies(),
  300000 // 5 minute TTL
);

const decision = await loader.evaluate(request);

// Force refresh
loader.invalidate();

Event-Driven Policy Updates

Use events to trigger policy reloads when changes occur.

import { EventEmitter } from 'events';
import { ABACEngine, ABACPolicy } from 'abac-engine';

export class EventDrivenPolicyManager extends EventEmitter {
  private engine: ABACEngine;

  constructor(initialPolicies: ABACPolicy[] = []) {
    super();
    this.engine = new ABACEngine({ policies: initialPolicies });
  }

  async updatePolicies(policies: ABACPolicy[]): Promise<void> {
    this.engine = new ABACEngine({ policies });
    this.emit('policies-updated', policies.length);
  }

  async addPolicy(policy: ABACPolicy): Promise<void> {
    // Load current policies, add new one, recreate engine
    const currentPolicies = this.engine['policies']; // Access private field
    const updatedPolicies = [...currentPolicies, policy];
    await this.updatePolicies(updatedPolicies);
    this.emit('policy-added', policy.id);
  }

  async removePolicy(policyId: string): Promise<void> {
    const currentPolicies = this.engine['policies'];
    const updatedPolicies = currentPolicies.filter(p => p.id !== policyId);
    await this.updatePolicies(updatedPolicies);
    this.emit('policy-removed', policyId);
  }

  getEngine(): ABACEngine {
    return this.engine;
  }
}

// Usage
const manager = new EventDrivenPolicyManager();

manager.on('policies-updated', (count) => {
  console.log(`${count} policies loaded`);
});

manager.on('policy-added', (id) => {
  console.log(`Policy ${id} added`);
});

// API endpoint to update policies
app.post('/api/policies', async (req, res) => {
  const policy = req.body;
  await repository.savePolicy(policy);
  await manager.addPolicy(policy);
  res.json({ success: true });
});

Best Practices

Version Control Policies

Store policy files in Git alongside your code. This provides version history, rollback capabilities, and change tracking.

Maintain Version History

Keep a version history table/collection to track policy changes over time. This is crucial for auditing and debugging.

Use Graceful Reload

When reloading policies, validate them first and maintain the old engine if validation fails. Don't break running services.

Optimize Load Frequency

Balance between freshness and performance. Don't reload too frequently in production - every 1-5 minutes is usually sufficient.

Index Your Queries

If using a database, create indexes on isActive, priority, and category fields for optimal query performance.

Backup Policies Regularly

Always maintain backups of your policies. Consider automated daily backups to prevent data loss.

Next Steps