inou/docs/rbac-design-2026-01.md

7.4 KiB

RBAC & Family Tree Redesign

Date: January 2026 Status: Design phase


Overview

Redesign of the permission and family relationship model to:

  1. Properly model genetic relationships via family tree
  2. Implement granular, entry-level RBAC
  3. Support custom roles with canned presets
  4. 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

  1. User selects a role (template) or creates custom
  2. System copies template rows with grantee_id set
  3. 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.go
  • api/api_*.go (14 files)
  • lib/data.go, lib/v2.go, lib/prompt.go
  • portal/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.)
  • lineage field (never fully implemented)

Migration Steps

  1. Add mother_id, father_id to dossiers
  2. Create access table
  3. Migrate existing dossier_accessaccess:
    • can_edit=true → ops includes 'w'
    • is_care_receiver=true → ops='rwdm'
    • role from relation label
  4. Consolidate SQL into store.go
  5. Add ctx parameter throughout
  6. Deprecate dossier_access table

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

  1. How to handle "link existing dossier" for family tree? Merge conflicts?
  2. Performance of tree traversal for permission checks - need benchmarks
  3. UI for custom role creation - how complex?
  4. Backward compatibility period - dual-write?

11. Implementation Order

  1. Design review & sign-off
  2. Consolidate SQL into store.go
  3. Add access table
  4. Implement permission cache
  5. Add mother_id/father_id to dossiers
  6. Migrate existing permissions
  7. Update portal UI
  8. Update API
  9. Update MCP bridge
  10. Deprecate old model