| Crates.io | permitheus |
| lib.rs | permitheus |
| version | 0.2.0 |
| created_at | 2025-11-09 05:06:01.343894+00 |
| updated_at | 2025-11-09 05:06:01.343894+00 |
| description | Fast hierarchical permission system with inheritance, delegation, and conflict resolution |
| homepage | |
| repository | |
| max_upload_size | |
| id | 1923661 |
| size | 144,764 |
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.
[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());
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));
// 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")));
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.
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
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 provide an indirection layer for managing permissions across many users. However, they add complexity:
Best Use Cases:
Considerations:
ConflictResolution)The library provides add_user_to_group() and group permission management. Conflicts are detected and resolved at runtime according to your chosen policy.
Permitheus is an in-memory permission manager. It does not handle persistence directly - you're responsible for storing and loading the permission data.
You need to persist two pieces of state:
PermissionEntry structs (user, group, and public permissions)UserGroupRelation structs (which users belong to which groups)ConflictResolution strategydump() with SerdeEnable 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.
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)?;
}
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
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,
})
}
}
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');
entry_id FieldThe 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.
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.
// 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) {
// ...
}
// 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)
// 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.
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
}
See examples/ for:
profile_manual.rs - Performance profiling and benchmarkingprofile_hotpath.rs - Detailed permission checking scenariosRun with: cargo run --release --example profile_manual
cargo test # Run all tests
cargo bench # Run performance benchmarks
All tests pass with the cache enabled, ensuring correctness is maintained.
MIT OR Apache-2.0 (your choice)
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.