From f7e6c32e30600addcfd87d09fc35ea6d3fc7d365 Mon Sep 17 00:00:00 2001 From: James Date: Sat, 7 Feb 2026 17:01:59 -0500 Subject: [PATCH] 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 --- lib/access.go | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/lib/access.go b/lib/access.go index ad13324..66bfe5a 100644 --- a/lib/access.go +++ b/lib/access.go @@ -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 "" }