7.4 KiB
RBAC & Family Tree Redesign
Date: January 2026 Status: Design phase
Overview
Redesign of the permission and family relationship model to:
- Properly model genetic relationships via family tree
- Implement granular, entry-level RBAC
- Support custom roles with canned presets
- Enforce all access control at the store layer
1. Family Tree (Genetics)
Schema Change
Add to dossiers table:
ALTER TABLE dossiers ADD COLUMN mother_id TEXT REFERENCES dossiers(dossier_id);
ALTER TABLE dossiers ADD COLUMN father_id TEXT REFERENCES dossiers(dossier_id);
Derived Relationships
All relationships computed via graph traversal:
| Relationship | Derivation |
|---|---|
| Children | WHERE mother_id = X OR father_id = X |
| Siblings | Share at least one parent |
| Full sibling | Share both parents |
| Half-sibling (maternal) | Share mother only |
| Half-sibling (paternal) | Share father only |
| Grandparents | Parent's mother_id / father_id |
| Cousins, aunts, etc. | Graph traversal |
UI
Each dossier shows:
Mother: [Name] [view]
Father: [_____] [+ add] [link: ____]
- + add: Create new dossier, link as parent
- link: Enter existing dossier ID to link
2. RBAC Model
Four Operations
| Op | Code | Description |
|---|---|---|
| Read | r | View entry and children |
| Write | w | Add/edit entries |
| Delete | d | Remove entries |
| Manage | m | Share access, grant permissions |
Access Levels
| Level | R | W | D | M |
|---|---|---|---|---|
| Owner (self) | always | always | always | always |
| Can-manage flag | always | always | always | always |
| Entry grant | per grant | per grant | no | no |
3. Schema: access Table
Single flexible table for all access control:
CREATE TABLE access (
id TEXT PRIMARY KEY,
dossier_id TEXT NOT NULL REFERENCES dossiers(dossier_id),
grantee_id TEXT REFERENCES dossiers(dossier_id), -- null = role template
entry_id TEXT REFERENCES entries(entry_id), -- null = root level
role TEXT NOT NULL, -- "Trainer", "Family", custom
ops TEXT NOT NULL, -- "r", "rw", "rwdm"
created_at INTEGER NOT NULL
);
CREATE INDEX idx_access_grantee ON access(grantee_id);
CREATE INDEX idx_access_dossier ON access(dossier_id);
CREATE INDEX idx_access_entry ON access(entry_id);
How It Works
| grantee_id | entry_id | role | ops | Meaning |
|---|---|---|---|---|
| null | null | "Trainer" | r | Template: Trainer default |
| null | {exercise_root} | "Trainer" | rw | Template: Trainer exercise access |
| {john} | null | "Trainer" | r | Grant: John as Trainer |
| {john} | {exercise_root} | "Trainer" | rw | Grant: John exercise write |
| {mary} | null | "Family" | rwdm | Grant: Mary full access |
Role Templates
grantee_id = null→ template definition- Templates are per-dossier (user creates their own)
- System templates:
dossier_id = null(Doctor, Coach, etc.)
Granting Access
- User selects a role (template) or creates custom
- System copies template rows with grantee_id set
- User can customize entry-level overrides
4. Permission Resolution
Algorithm
canAccess(accessor, dossier, entry, op):
1. if ctx.IsSystem → allow
2. if accessor == dossier owner → allow
3. lookup grants for (accessor, dossier)
4. if grant exists for entry with op → allow
5. walk up entry.parent_id, repeat step 4
6. if hit root (entry=null) and still no grant → deny
Inheritance
Access to a parent entry implies access to all children.
Example: Grant read on "Exercise" category → can read all exercise entries.
5. Caching Strategy (Option C)
Approach
Load grants lazily per accessor-dossier pair, cache in memory.
Implementation
type permCache struct {
// [dossier_id][entry_id] → "rwdm"
grants map[string]map[string]string
loadedAt time.Time
}
// Per accessor
var accessorCache = map[string]*permCache{}
var cacheTTL = 5 * time.Minute
Load
func loadPerms(accessorID string) *permCache {
// SELECT dossier_id, entry_id, ops
// FROM access
// WHERE grantee_id = ?
// Build map, return
}
Check
func canAccess(ctx, dossierID, entryID, op string) bool {
if ctx.IsSystem { return true }
if ctx.AccessorID == dossierOwner { return true }
perms := getPerms(ctx.AccessorID, dossierID)
return walkTreeCheckPerms(perms, entryID, op)
}
Invalidation
func invalidateCache(accessorID string) {
delete(accessorCache, accessorID)
}
// Called on any access table write
6. Store Layer Enforcement
Principle
All RBAC checks happen in store.go. Impossible to bypass.
Context
type AccessContext struct {
AccessorID string // who's asking
IsSystem bool // bypass RBAC
}
Wrapped Functions
| Current | New |
|---|---|
EntryGet(id) |
EntryGet(ctx, id) |
EntryList(filter) |
EntryList(ctx, filter) |
EntrySave(e) |
EntrySave(ctx, e) |
EntryDelete(id) |
EntryDelete(ctx, id) |
DossierGet(id) |
DossierGet(ctx, id) |
Prerequisite
Consolidate all SQL into store.go. Currently scattered across:
portal/main.goapi/api_*.go(14 files)lib/data.go,lib/v2.go,lib/prompt.goportal/prompts.go,portal/genome.go
7. Canned Role Presets
System Roles (dossier_id = null)
| Role | Default Ops | Entry Scope |
|---|---|---|
| Family | rwdm | root |
| Doctor | rw | root |
| Caregiver | rw | root |
| Trainer | r | root |
| Trainer | rw | exercise category |
| Trainer | rw | nutrition category |
| Friend | r | root |
Custom Roles
Users can create their own role templates with custom entry-level permissions.
8. Migration from Current Model
What Changes
| Current | New |
|---|---|
dossier_access.relation (family types) |
dossiers.mother_id/father_id |
dossier_access.relation (roles) |
access.role |
dossier_access.can_edit |
access.ops includes 'w' |
dossier_access.is_care_receiver |
access.ops includes 'm' |
What Gets Removed
- Family relation types in
dossier_access(Parent, Child, Sibling, etc.) lineagefield (never fully implemented)
Migration Steps
- Add
mother_id,father_idto dossiers - Create
accesstable - Migrate existing
dossier_access→access:can_edit=true→ ops includes 'w'is_care_receiver=true→ ops='rwdm'- role from relation label
- Consolidate SQL into store.go
- Add ctx parameter throughout
- Deprecate
dossier_accesstable
9. API Impact
MCP Bridge
Needs to pass accessor context from authentication.
REST API
Needs to pass accessor context from token.
Portal
Needs to create context from session.
10. Open Questions
- How to handle "link existing dossier" for family tree? Merge conflicts?
- Performance of tree traversal for permission checks - need benchmarks
- UI for custom role creation - how complex?
- Backward compatibility period - dual-write?
11. Implementation Order
- Design review & sign-off
- Consolidate SQL into store.go
- Add
accesstable - Implement permission cache
- Add
mother_id/father_idto dossiers - Migrate existing permissions
- Update portal UI
- Update API
- Update MCP bridge
- Deprecate old model