permitheus 0.2.0

Fast hierarchical permission system with inheritance, delegation, and conflict resolution
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
# 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:

| 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:

```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.