Policy

SwirlDB uses a JSON-based policy engine for fine-grained access control. Policies are defined as configuration rules with priority-based evaluation, path pattern matching with variable substitution, and support for role-based and custom authorization logic.

Policy Architecture

JSON-Based Configuration

All policies are defined in a JSON configuration file with these core concepts:

  • Actor: Who is performing the operation (User, App, Server, Anonymous)
  • Action: What operation is being performed (Read, Write, Subscribe)
  • Path Pattern: Which data is being accessed (supports wildcards and variables)
  • Effect: Allow or Deny
  • Priority: Lower numbers = higher priority (first match wins)

Configuration Structure

Basic Example

A minimal policy configuration:

{
  "policies": {
    "rules": [
      {
        "priority": 10,
        "actor": { "type": "User" },
        "action": "Read",
        "path_pattern": "/user/{actor.id}/**",
        "effect": "Allow"
      },
      {
        "priority": 100,
        "actor": { "type": "Any" },
        "action": "Read",
        "path_pattern": "/**",
        "effect": "Deny"
      }
    ]
  }
}

In this example:

  • Users can read their own data under /user/{actor.id}/**
  • All other reads are denied (priority 100 runs after priority 10)

Rule Structure

Each policy rule has these fields:

{
  "_description": "Optional description for documentation",
  "priority": 10,              // Lower = higher priority
  "actor": {...},              // Actor pattern
  "action": "Read" | "Write" | "Subscribe",
  "path_pattern": "/path/**",  // Path with wildcards
  "effect": "Allow" | "Deny"
}

Actor Patterns

User Actor

Match users, optionally filtering by ID or role:

// Any user
{ "type": "User" }

// Specific user ID
{ "type": "User", "id": "alice" }

// Users with specific role
{ "type": "User", "role": "admin" }

// Specific user with specific role
{ "type": "User", "id": "alice", "role": "editor" }

App and Server Actors

// Any app
{ "type": "App" }

// Specific app
{ "type": "App", "id": "mobile-client" }

// Any server instance
{ "type": "Server" }

// Specific server
{ "type": "Server", "id": "sync-coordinator" }

Special Actors

// Anonymous (no JWT)
{ "type": "Anonymous" }

// Any actor (matches all)
{ "type": "Any" }

Path Patterns

Wildcard Matching

Path patterns support two types of wildcards:

  • * — Matches exactly one path segment
  • ** — Matches zero or more path segments
// Exact match
"/config/version"

// Single wildcard (any organization)
"/org/*/members"

// Multi wildcard (any depth under /user)
"/user/**"

// Combined
"/org/*/teams/*/members"

Variable Substitution

Use actor attributes in path patterns:

// Match user's own data
"/user/{actor.id}/**"

// Match organization data
"/org/{actor.org_id}/**"

// Match team data
"/team/{actor.team_id}/**"

// Match app-specific data
"/app/{actor.app_id}/**"

// Role-based path
"/access/{actor.role}/**"

How it works: The engine substitutes {actor.id} with the actual actor's ID before matching.

Common Patterns

User Data Isolation

Users can only access their own data:

{
  "policies": {
    "rules": [
      {
        "priority": 10,
        "actor": { "type": "User" },
        "action": "Read",
        "path_pattern": "/user/{actor.id}/**",
        "effect": "Allow"
      },
      {
        "priority": 10,
        "actor": { "type": "User" },
        "action": "Write",
        "path_pattern": "/user/{actor.id}/**",
        "effect": "Allow"
      },
      {
        "priority": 100,
        "actor": { "type": "Any" },
        "action": "Read",
        "path_pattern": "/**",
        "effect": "Deny"
      },
      {
        "priority": 100,
        "actor": { "type": "Any" },
        "action": "Write",
        "path_pattern": "/**",
        "effect": "Deny"
      }
    ]
  }
}

Role-Based Access Control

Different permissions per role:

{
  "policies": {
    "rules": [
      {
        "_description": "Admins can do anything",
        "priority": 1,
        "actor": { "type": "User", "role": "admin" },
        "action": "Write",
        "path_pattern": "/**",
        "effect": "Allow"
      },
      {
        "_description": "Editors can write documents",
        "priority": 10,
        "actor": { "type": "User", "role": "editor" },
        "action": "Write",
        "path_pattern": "/documents/**",
        "effect": "Allow"
      },
      {
        "_description": "All users can read",
        "priority": 20,
        "actor": { "type": "User" },
        "action": "Read",
        "path_pattern": "/**",
        "effect": "Allow"
      },
      {
        "_description": "Default deny",
        "priority": 1000,
        "actor": { "type": "Any" },
        "action": "Write",
        "path_pattern": "/**",
        "effect": "Deny"
      }
    ]
  }
}

Organization Isolation (Multi-Tenancy)

Users can only access their organization's data:

{
  "policies": {
    "rules": [
      {
        "priority": 10,
        "actor": { "type": "User" },
        "action": "Read",
        "path_pattern": "/org/{actor.org_id}/**",
        "effect": "Allow"
      },
      {
        "priority": 10,
        "actor": { "type": "User" },
        "action": "Write",
        "path_pattern": "/org/{actor.org_id}/**",
        "effect": "Allow"
      },
      {
        "priority": 100,
        "actor": { "type": "Any" },
        "action": "Read",
        "path_pattern": "/**",
        "effect": "Deny"
      }
    ]
  }
}

Public Read, Private Write

Anyone can read, only authenticated users can write:

{
  "policies": {
    "rules": [
      {
        "priority": 10,
        "actor": { "type": "Any" },
        "action": "Read",
        "path_pattern": "/**",
        "effect": "Allow"
      },
      {
        "priority": 10,
        "actor": { "type": "User" },
        "action": "Write",
        "path_pattern": "/**",
        "effect": "Allow"
      },
      {
        "priority": 100,
        "actor": { "type": "Anonymous" },
        "action": "Write",
        "path_pattern": "/**",
        "effect": "Deny"
      }
    ]
  }
}

Field-Level Permissions

Different permissions for different fields:

{
  "policies": {
    "rules": [
      {
        "_description": "Public profile fields - anyone can read",
        "priority": 5,
        "actor": { "type": "Any" },
        "action": "Read",
        "path_pattern": "/user/*/name",
        "effect": "Allow"
      },
      {
        "_description": "Private fields - only owner",
        "priority": 10,
        "actor": { "type": "User" },
        "action": "Read",
        "path_pattern": "/user/{actor.id}/email",
        "effect": "Allow"
      },
      {
        "_description": "Deny all other email access",
        "priority": 100,
        "actor": { "type": "Any" },
        "action": "Read",
        "path_pattern": "/user/*/email",
        "effect": "Deny"
      }
    ]
  }
}

Loading Configuration

Rust

use swirldb_core::policy::PolicyEngine;

// From JSON string
let config_json = std::fs::read_to_string("policy.json")?;
let engine = PolicyEngine::from_json(&config_json)?;

// Evaluate a policy
let decision = engine.evaluate(&actor, Action::Read, "/user/alice/prefs");
if decision.is_allowed() {
    println!("Access granted!");
} else {
    println!("Access denied!");
}

TypeScript/Browser

// Load configuration
const config = await fetch('/policy.json').then(r => r.json());
const db = await SwirlDB.withConfig(config);

// Access is automatically checked on all operations
try {
  const data = db.data.user.alice.prefs.$value;
  console.log('Access granted:', data);
} catch (error) {
  console.error('Access denied:', error);
}

Actor Creation

From JWT Claims

Typically, actors are created from JWT tokens on the server:

use swirldb_core::policy::Actor;
use std::collections::HashMap;

// Extract claims from JWT
let mut claims = HashMap::new();
claims.insert("type".to_string(), json!("user"));
claims.insert("sub".to_string(), json!("alice"));
claims.insert("org_id".to_string(), json!("acme-corp"));
claims.insert("role".to_string(), json!("admin"));

let actor = Actor::from_jwt_claims(claims)?;

// Now use actor in policy evaluation
let decision = engine.evaluate(&actor, Action::Write, "/org/acme-corp/docs");

Anonymous Actor

For unauthenticated requests:

let actor = Actor::anonymous();

// This actor will only match:
// { "type": "Anonymous" }
// { "type": "Any" }

Best Practices

Priority Guidelines

  • 1-10: Highest priority, admin overrides
  • 10-50: Specific permissions (user/org/role-based)
  • 100-500: General permissions
  • 1000+: Default deny rules

Always use default deny at the end with high priority number.

Path Organization

  • Use consistent path hierarchies: /user/{id}/resource
  • Put tenant/org ID early in path: /org/{org_id}/data
  • Group related data: /team/{id}/projects/**
  • Use descriptive segments: /documents/public/**

Security

  • Deny by default: Always end with a catch-all deny rule
  • Server-side enforcement: Never trust client-side policy checks
  • Validate JWT: Always verify token signature before creating actor
  • Test thoroughly: Unit test all policy rules with various actors

Full Configuration Example

{
  "policies": {
    "rules": [
      {
        "_description": "Admins can do everything",
        "priority": 1,
        "actor": { "type": "User", "role": "admin" },
        "action": "Write",
        "path_pattern": "/**",
        "effect": "Allow"
      },
      {
        "_description": "Admins can read everything",
        "priority": 1,
        "actor": { "type": "User", "role": "admin" },
        "action": "Read",
        "path_pattern": "/**",
        "effect": "Allow"
      },
      {
        "_description": "Users can read their own data",
        "priority": 10,
        "actor": { "type": "User" },
        "action": "Read",
        "path_pattern": "/user/{actor.id}/**",
        "effect": "Allow"
      },
      {
        "_description": "Users can write their own data",
        "priority": 10,
        "actor": { "type": "User" },
        "action": "Write",
        "path_pattern": "/user/{actor.id}/**",
        "effect": "Allow"
      },
      {
        "_description": "Users can read org data",
        "priority": 20,
        "actor": { "type": "User" },
        "action": "Read",
        "path_pattern": "/org/{actor.org_id}/**",
        "effect": "Allow"
      },
      {
        "_description": "Editors can write org documents",
        "priority": 20,
        "actor": { "type": "User", "role": "editor" },
        "action": "Write",
        "path_pattern": "/org/{actor.org_id}/documents/**",
        "effect": "Allow"
      },
      {
        "_description": "Public data readable by all",
        "priority": 50,
        "actor": { "type": "Any" },
        "action": "Read",
        "path_pattern": "/public/**",
        "effect": "Allow"
      },
      {
        "_description": "Default deny reads",
        "priority": 1000,
        "actor": { "type": "Any" },
        "action": "Read",
        "path_pattern": "/**",
        "effect": "Deny"
      },
      {
        "_description": "Default deny writes",
        "priority": 1000,
        "actor": { "type": "Any" },
        "action": "Write",
        "path_pattern": "/**",
        "effect": "Deny"
      }
    ]
  }
}