permitheus

Crates.iopermitheus
lib.rspermitheus
version0.2.0
created_at2025-11-09 05:06:01.343894+00
updated_at2025-11-09 05:06:01.343894+00
descriptionFast hierarchical permission system with inheritance, delegation, and conflict resolution
homepage
repository
max_upload_size
id1923661
size144,764
Julian Stahl (ThinkingJoules)

documentation

https://docs.rs/permitheus

README

Permitheus

A fast, hierarchical permission system for Rust with inheritance, delegation, and conflict resolution. Perfect for building file systems, document management systems, or any application requiring fine-grained access control.

Features

  • Hierarchical Permissions: Permissions inherit from parent paths (like Unix file systems)
  • Three-tier Model: User, Group, and Public permissions with configurable precedence
  • Delegation: Users can grant permissions to others with optional constraints
  • Conflict Resolution: Configurable policies for handling group permission conflicts
  • High Performance: LRU cache delivers 16-83x speedup (40-50ns per permission check)
  • Type-safe: Generic over user and group identifier types
  • Zero-copy Iterators: Efficient permission enumeration
  • Optional Serde Support: Serialize/deserialize permission state with one line of code

Quick Start

[dependencies]
permitheus = "0.2"

# Optional: enable serde support for easy serialization
permitheus = { version = "0.2", features = ["serde"] }
use permitheus::*;

// Bootstrap with a system user
let system_entry = PermissionEntry::new(
    PermissionsInfo::new(
        PermissionMode::Allow,
        Permissions::newrwx(),
        None,
        Some(Permissions::newrwx()),
    ),
    ResourcePath::new("/"),
    "system",
    None,
    Entity::User("system"),
);

let mut manager: PermissionsManager<&str, &str> = PermissionsManager::new(
    vec![system_entry],
    vec![],
    ConflictResolution::DenyWins,
);

// Grant permissions
let entry = PermissionEntry::new(
    PermissionsInfo::new(
        PermissionMode::Allow,
        Permissions::newrw(),
        None,
        None,
    ),
    ResourcePath::new("/documents"),
    "system",
    None,
    Entity::User("alice"),
);
manager.add_entry(entry).unwrap();

// Check permissions
let result = manager.request_allowed(
    &ResourcePath::new("/documents/file.txt"),
    Some(&"alice"),
    None,
    &Permission::Read,
);
assert!(result.is_allowed());

Core Concepts

Hierarchical Inheritance

Permissions on /documents automatically apply to /documents/file.txt unless a more specific permission exists at a deeper path. This matches how filesystems work:

// Permission at /documents applies to all children
manager.add_entry(allow_rw("/documents", "alice"));

// alice can read /documents/2024/report.pdf (inherited)
assert!(manager.user_can_access("/documents/2024/report.pdf", &"alice", &Permission::Read));

// More specific permission overrides
manager.add_entry(deny_all("/documents/private", "alice"));

// Now alice CANNOT read /documents/private/secret.txt
assert!(!manager.user_can_access("/documents/private/secret.txt", &"alice", &Permission::Read));

Three Permission Tiers

  1. User Permissions: Direct grants to specific users (highest priority for that user)
  2. Group Permissions: Grants to groups that users belong to
  3. Public Permissions: Available to everyone, including unauthenticated users
// Public: anyone can read
manager.add_entry(allow_read("/public", Entity::Public));

// Group: engineers can write
manager.add_user_to_group("alice", "engineers");
manager.add_entry(allow_rw("/projects", Entity::Group("engineers")));

// User: specific override
manager.add_entry(deny_write("/projects/sensitive", Entity::User("alice")));

Conflict Resolution

When a user belongs to multiple groups with conflicting permissions, the configured policy determines the outcome:

// alice is in both "viewers" (allow read) and "restricted" (deny read)
manager.add_user_to_group("alice", "viewers");
manager.add_user_to_group("alice", "restricted");

// With DenyWins (default, most secure):
let manager = PermissionsManager::new(data, groups, ConflictResolution::DenyWins);
// → Deny access if ANY group denies

// With AllowWins (most permissive):
let manager = PermissionsManager::new(data, groups, ConflictResolution::AllowWins);
// → Allow access if ANY group allows

// With ConflictDenies (fail-safe):
let manager = PermissionsManager::new(data, groups, ConflictResolution::ConflictDenies);
// → Deny access when conflicts detected

All conflicts are logged in the PermissionResult::Conflict variant for auditing.

Delegation

Users can grant permissions to others, with optional constraints:

// alice gets rwx with ability to share read
let entry = PermissionEntry::new(
    PermissionsInfo::new(
        PermissionMode::Allow,
        Permissions::newrwx(),
        None,
        Some(Permissions::newr()), // Can only share 'read'
    ),
    ResourcePath::new("/her-folder"),
    "admin",
    None,
    Entity::User("alice"),
);
manager.add_entry(entry).unwrap();

// alice can now grant read to bob
let grant = PermissionEntry::new(
    PermissionsInfo::new(
        PermissionMode::Allow,
        Permissions::newr(),
        None,
        None,
    ),
    ResourcePath::new("/her-folder"),
    "alice", // alice is the grantor
    None,
    Entity::User("bob"),
);
manager.add_entry(grant).unwrap(); // ✓ Succeeds

// But alice cannot grant write (not in her shareable set)
let invalid = PermissionEntry::new(
    PermissionsInfo::new(
        PermissionMode::Allow,
        Permissions::neww(),
        None,
        None,
    ),
    ResourcePath::new("/her-folder"),
    "alice",
    None,
    Entity::User("bob"),
);
manager.add_entry(invalid).unwrap_err(); // ✗ Returns GrantorLacksPermission

Performance

Permitheus uses an LRU cache (1000 entries) that is automatically managed:

Operation Uncached Cached Speedup
Direct user permission 718ns 43ns 16.7x
Group permission 774ns 44ns 17.6x
Deep path (not found) 4,160ns 50ns 83x

The cache is automatically cleared when permissions or group memberships change, ensuring consistency while delivering excellent performance for read-heavy workloads (typical in file systems).

Throughput: ~20-25 million permission checks/second (cached), ~1.3 million checks/second (uncached)

Groups

Groups provide an indirection layer for managing permissions across many users. However, they add complexity:

Best Use Cases:

  • Simple, stable roles (e.g., "admins", "editors", "viewers")
  • Large numbers of users sharing similar access patterns
  • Organizational structure maps cleanly to folder hierarchy

Considerations:

  • Multiple group memberships can create conflicts (handled by ConflictResolution)
  • "Share as group" vs "share as user" - group shares can be revoked by any group member
  • If your structure doesn't align with groups, user-based permissions are simpler

The library provides add_user_to_group() and group permission management. Conflicts are detected and resolved at runtime according to your chosen policy.

Persistence

Permitheus is an in-memory permission manager. It does not handle persistence directly - you're responsible for storing and loading the permission data.

What Needs to be Persisted

You need to persist two pieces of state:

  1. Permission Entries - All PermissionEntry structs (user, group, and public permissions)
  2. Group Memberships - All UserGroupRelation structs (which users belong to which groups)
  3. Conflict Policy - Your chosen ConflictResolution strategy

Simple Approach: Using dump() with Serde

Enable the serde feature for automatic serialization:

[dependencies]
permitheus = { version = "0.2", features = ["serde"] }
serde_json = "1.0"  # or serde_cbor, bincode, etc.
use permitheus::*;
use std::fs;

// Shutdown: export and serialize
let dump = manager.dump();
let json = serde_json::to_string(&dump)?;
fs::write("permissions.json", json)?;

// Startup: deserialize and restore
let json = fs::read_to_string("permissions.json")?;
let (entries, groups, policy) = serde_json::from_str(&json)?;
let manager = PermissionsManager::new(entries, groups, policy);

Note: This requires your U and G types to implement Serialize and Deserialize. Standard types like String, u64, Uuid all work out of the box.

Advanced Approach: Manual Extraction

Extract the current state and save it to your database/storage:

// Option 1: Manual extraction (if you track changes)
// You already have the entries and groups from your DB

// Option 2: Export current state (if you need to dump everything)
// Note: The library doesn't provide an export method, so you'd track
// entries yourself as you add/remove them

struct PersistedState {
    entries: Vec<PermissionEntry<UserId, GroupId>>,
    group_memberships: Vec<UserGroupRelation<UserId, GroupId>>,
    conflict_policy: ConflictResolution,
}

// Save to database
let state = PersistedState {
    entries: my_tracked_entries, // You maintain this list
    group_memberships: my_tracked_groups,
    conflict_policy: ConflictResolution::DenyWins,
};

// Serialize and save (example with serde_json)
let json = serde_json::to_string(&state)?;
std::fs::write("permissions.json", json)?;

// Or save to SQL database
for entry in state.entries {
    db.execute("INSERT INTO permissions (...) VALUES (...)", entry)?;
}

Startup Routine

Load state from storage and reconstruct the manager:

// Load from database
let entries: Vec<PermissionEntry<UserId, GroupId>> =
    db.query("SELECT * FROM permissions")?
      .into_iter()
      .map(|row| /* construct PermissionEntry from row */)
      .collect();

let groups: Vec<UserGroupRelation<UserId, GroupId>> =
    db.query("SELECT user_id, group_id FROM group_memberships")?
      .into_iter()
      .map(|row| UserGroupRelation::new(row.user_id, row.group_id))
      .collect();

// Reconstruct the manager
let manager = PermissionsManager::new(
    entries,
    groups,
    ConflictResolution::DenyWins, // Load from config
);

// Manager is ready to use, cache starts empty

Recommended Pattern: Track Changes

Since the library doesn't provide state export, maintain a parallel list of changes:

struct PermissionStore {
    manager: PermissionsManager<UserId, GroupId>,
    // Track all entries for persistence
    entries: HashMap<(ResourcePath, Entity<UserId, GroupId>), PermissionEntry<UserId, GroupId>>,
    groups: HashMap<UserId, HashSet<GroupId>>,
}

impl PermissionStore {
    fn add_entry(&mut self, entry: PermissionEntry<UserId, GroupId>) -> Result<(), AddEntryError> {
        // Add to manager
        self.manager.add_entry(entry.clone())?;

        // Track for persistence
        let key = (entry.path.clone(), entry.grantee.clone());
        self.entries.insert(key, entry.clone());

        // Persist to database
        self.db.insert_permission(&entry)?;

        Ok(())
    }

    fn remove_entry(&mut self, revoker: &UserId, path: &ResourcePath, grantee: &Entity<UserId, GroupId>) -> Result<(), RemoveEntryError> {
        // Remove from manager
        let removed = self.manager.remove_entry(revoker, path, grantee)?;

        // Remove from tracking
        let key = (path.clone(), grantee.clone());
        self.entries.remove(&key);

        // Persist to database
        self.db.delete_permission(path, grantee)?;

        Ok(())
    }

    fn shutdown(&self) -> Result<(), Error> {
        // State is already persisted incrementally
        Ok(())
    }

    fn startup(db: &Database) -> Result<Self, Error> {
        let entries: Vec<_> = db.load_all_permissions()?;
        let groups: Vec<_> = db.load_all_group_memberships()?;

        let manager = PermissionsManager::new(
            entries.clone(),
            groups.clone(),
            ConflictResolution::DenyWins,
        );

        // Build tracking structures
        let entry_map = entries.into_iter()
            .map(|e| ((e.path.clone(), e.grantee.clone()), e))
            .collect();

        let group_map = groups.into_iter()
            .fold(HashMap::new(), |mut acc, rel| {
                acc.entry(rel.user).or_insert_with(HashSet::new).insert(rel.group);
                acc
            });

        Ok(Self {
            manager,
            entries: entry_map,
            groups: group_map,
        })
    }
}

Database Schema Example

CREATE TABLE permissions (
    id INTEGER PRIMARY KEY,
    path TEXT NOT NULL,
    grantee_type TEXT NOT NULL, -- 'user', 'group', or 'public'
    grantee_id TEXT,            -- NULL for public
    mode TEXT NOT NULL,          -- 'allow' or 'deny'
    read BOOLEAN NOT NULL,
    write BOOLEAN NOT NULL,
    execute BOOLEAN NOT NULL,
    grantor_id TEXT NOT NULL,
    grantor_as_group TEXT,
    shareable_read BOOLEAN,
    shareable_write BOOLEAN,
    shareable_execute BOOLEAN,
    expiration_secs INTEGER,
    entry_id TEXT,              -- Application-defined ID
    UNIQUE(path, grantee_type, grantee_id)
);

CREATE TABLE group_memberships (
    user_id TEXT NOT NULL,
    group_id TEXT NOT NULL,
    PRIMARY KEY (user_id, group_id)
);

CREATE TABLE config (
    key TEXT PRIMARY KEY,
    value TEXT NOT NULL
);

INSERT INTO config (key, value) VALUES ('conflict_policy', 'deny_wins');

The entry_id Field

The entry_id field in PermissionEntry is provided for your use:

pub struct PermissionEntry<U, G> {
    // ... other fields ...
    pub entry_id: Option<String>, // Your database primary key
}

Use it to track which database row corresponds to which permission entry.

Why in-memory? Different applications have different persistence needs (SQL, NoSQL, file-based, event sourcing). Permitheus focuses on fast, correct permission logic and lets you choose your storage layer.

Resource Management

Since Permitheus doesn't know about your actual resources (files, documents, etc.), it provides utilities for keeping permissions in sync:

// Delete a folder and all permissions beneath it
manager.delete_resource_recursive(&ResourcePath::new("/old-project"));

// Move a resource, preserving its permissions
manager.move_resource(
    &ResourcePath::new("/drafts/document"),
    &ResourcePath::new("/published/document"),
);

Moving resources preserves existing permissions and inherits from the new parent path.

API Overview

Permission Checking

// Detailed result with conflict information
let result: PermissionResult = manager.request_allowed(resource, user, group, permission);
match result {
    PermissionResult::Allowed => { /* grant access */ }
    PermissionResult::Denied => { /* deny access */ }
    PermissionResult::NotFound => { /* no permission defined */ }
    PermissionResult::Conflict { resolved, conflicting_groups, .. } => {
        // Log conflict for audit
        if resolved { /* grant */ } else { /* deny */ }
    }
}

// Simple boolean check
if manager.user_can_access(resource, &user, &Permission::Read) {
    // ...
}

// Check public access
if manager.is_public(resource, &Permission::Read) {
    // ...
}

Permission Management

// Add permission entry
manager.add_entry(entry)?; // Returns AddEntryError::GrantorLacksPermission

// Remove permission entry
manager.remove_entry(revoker, resource, grantee)?; // Returns RemoveEntryError

// Modify group membership
manager.add_user_to_group(user, group); // Returns bool (was added)
manager.remove_user_from_group(user, group); // Returns bool (was removed)

Querying

// List all permission entries for a user (user + group, not public)
for (path, entry) in manager.list_user_permissions(&user) {
    println!("{}: {:?}", path, entry.permissions());
}

Note: This returns raw entries, not effective permissions after inheritance. Use request_allowed() to check effective permissions for a specific resource.

Error Handling

Permitheus uses type-safe errors:

pub enum AddEntryError {
    GrantorLacksPermission, // Grantor cannot grant these permissions
}

pub enum RemoveEntryError {
    EntryNotFound,  // No entry exists at this path
    NotAuthorized,  // Revoker is not in the grantor chain
}

Examples

See examples/ for:

  • profile_manual.rs - Performance profiling and benchmarking
  • profile_hotpath.rs - Detailed permission checking scenarios

Run with: cargo run --release --example profile_manual

Testing

cargo test      # Run all tests
cargo bench     # Run performance benchmarks

All tests pass with the cache enabled, ensuring correctness is maintained.

License

MIT OR Apache-2.0 (your choice)

Status

Experimental but functional. Used for personal projects. Breaking changes may occur.


Built for a Google Drive-like clone where permission checking happens on every file access. Optimized for read-heavy workloads with occasional writes.

Commit count: 0

cargo fmt