# 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
```toml
[dependencies]
permitheus = "0.2"
# Optional: enable serde support for easy serialization
permitheus = { version = "0.2", features = ["serde"] }
```
```rust
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:
```rust
// 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
```rust
// 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:
```rust
// 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:
```rust
// 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:
| 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:
```toml
[dependencies]
permitheus = { version = "0.2", features = ["serde"] }
serde_json = "1.0" # or serde_cbor, bincode, etc.
```
```rust
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:
```rust
// 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:
```rust
// 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:
```rust
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
```sql
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:
```rust
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:
```rust
// 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
```rust
// 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
```rust
// 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
```rust
// 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:
```rust
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
```bash
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.