refactor: simplify RBAC - categories are entries

- Remove special cat:{id} handling from permission resolution
- Categories are now just entries with parent_id=""
- Access flows naturally through parent_id chain hierarchy
- Three levels: root (entry_id="") > categories > individual entries
- Explicit denial supported with ops=""
- Updated documentation to reflect cleaner model

Next: deprecate dossier_access table, migrate to access grants

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
James 2026-02-07 17:01:59 -05:00
parent c1cd76559d
commit f7e6c32e30
1 changed files with 15 additions and 19 deletions

View File

@ -7,17 +7,23 @@ import (
)
// ============================================================================
// RBAC Access Control - Rock-solid permission enforcement
// RBAC Access Control - Entry-based permission system
// ============================================================================
//
// Permission checks happen at the LOWEST LEVEL in v2.go functions.
// There is NO WAY to bypass RBAC - every data access function checks permissions.
// Three-level hierarchy:
// 1. Root (entry_id = "") - "all" or "nothing"
// 2. Categories - entries that are category roots (parent_id = "")
// 3. Individual entries - access flows down via parent_id chain
//
// Operations:
// r = read - view data
// w = write - create/update data
// d = delete - remove data
// m = manage - grant/revoke access to others
// "" = explicit denial (removes inherited access)
//
// Categories are just entries - no special handling.
// Access to parent implies access to all children.
//
// ============================================================================
@ -196,7 +202,8 @@ func getEffectiveOps(accessorID, dossierID, entryID string) string {
}
// findMatchingOps finds the most specific grant that applies to entryID
// Priority: entry-specific > parent chain > category > root
// Priority: entry-specific > parent chain > root
// Categories are just entries - no special handling needed
func findMatchingOps(grants []*Access, entryID string) string {
// Build entry->ops map for quick lookup
grantMap := make(map[string]string) // entry_id -> ops (empty key = root)
@ -206,47 +213,36 @@ func findMatchingOps(grants []*Access, entryID string) string {
grantMap[g.EntryID] = mergeOps(existing, g.Ops)
}
// 1. Check entry-specific grant
// 1. Check entry-specific grant (including category entries)
if entryID != "" {
if ops, ok := grantMap[entryID]; ok {
return ops
}
// 2. Walk up parent chain (using raw function to avoid RBAC recursion)
// Also track entry category for category grant check
var entryCategory int
currentID := entryID
for i := 0; i < 100; i++ { // max depth to prevent infinite loops
entry, err := entryGetRaw(currentID)
if err != nil || entry == nil {
break
}
if i == 0 {
entryCategory = entry.Category
}
if entry.ParentID == "" {
break
}
// Check parent for grant
if ops, ok := grantMap[entry.ParentID]; ok {
return ops
}
currentID = entry.ParentID
}
// 3. Check category grant (cat:{category_id})
if entryCategory > 0 {
catKey := fmt.Sprintf("cat:%d", entryCategory)
if ops, ok := grantMap[catKey]; ok {
return ops
}
}
}
// 4. Check root grant
// 3. Check root grant (entry_id = "" means "all")
if ops, ok := grantMap[""]; ok {
return ops
}
// 4. No grant found
return ""
}