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:
parent
c1cd76559d
commit
f7e6c32e30
|
|
@ -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.
|
// Three-level hierarchy:
|
||||||
// There is NO WAY to bypass RBAC - every data access function checks permissions.
|
// 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:
|
// Operations:
|
||||||
// r = read - view data
|
// r = read - view data
|
||||||
// w = write - create/update data
|
// w = write - create/update data
|
||||||
// d = delete - remove data
|
// d = delete - remove data
|
||||||
// m = manage - grant/revoke access to others
|
// 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
|
// 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 {
|
func findMatchingOps(grants []*Access, entryID string) string {
|
||||||
// Build entry->ops map for quick lookup
|
// Build entry->ops map for quick lookup
|
||||||
grantMap := make(map[string]string) // entry_id -> ops (empty key = root)
|
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)
|
grantMap[g.EntryID] = mergeOps(existing, g.Ops)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Check entry-specific grant
|
// 1. Check entry-specific grant (including category entries)
|
||||||
if entryID != "" {
|
if entryID != "" {
|
||||||
if ops, ok := grantMap[entryID]; ok {
|
if ops, ok := grantMap[entryID]; ok {
|
||||||
return ops
|
return ops
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Walk up parent chain (using raw function to avoid RBAC recursion)
|
// 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
|
currentID := entryID
|
||||||
for i := 0; i < 100; i++ { // max depth to prevent infinite loops
|
for i := 0; i < 100; i++ { // max depth to prevent infinite loops
|
||||||
entry, err := entryGetRaw(currentID)
|
entry, err := entryGetRaw(currentID)
|
||||||
if err != nil || entry == nil {
|
if err != nil || entry == nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if i == 0 {
|
|
||||||
entryCategory = entry.Category
|
|
||||||
}
|
|
||||||
if entry.ParentID == "" {
|
if entry.ParentID == "" {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
// Check parent for grant
|
||||||
if ops, ok := grantMap[entry.ParentID]; ok {
|
if ops, ok := grantMap[entry.ParentID]; ok {
|
||||||
return ops
|
return ops
|
||||||
}
|
}
|
||||||
currentID = entry.ParentID
|
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 {
|
if ops, ok := grantMap[""]; ok {
|
||||||
return ops
|
return ops
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. No grant found
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue