inou/docs/store-consolidation-plan.md

211 lines
8.6 KiB
Markdown

# Store Consolidation Plan
Goal: Route all database access through `lib/store.go` to enable RBAC enforcement at a single layer.
## Current State
| Location | Raw SQL Ops | Notes |
|----------|-------------|-------|
| lib/data.go | 26 | Core domain functions (Entry, Dossier, Access) |
| lib/store.go | 6 | Generic ORM layer - this becomes the single point |
| lib/prompt.go | 8 | Prompt CRUD |
| lib/db.go | 5 | Helper wrappers |
| lib/v2.go | 1 | Cleanup utility |
| api/api_access.go | 5 | Access control queries |
| api/api_genome.go | 4 | Complex JOINs for variant queries |
| api/api_categories.go | 7 | Schema introspection (PRAGMA) |
| api/api_dossier.go | 4 | Dossier queries |
| portal/main.go | 6 | Rate limiting, sessions |
**Total: 76 raw SQL operations**
## Target Architecture
```
┌─────────────────────────────────────────────────────────┐
│ API / Portal │
│ (no db.Query, db.Exec - only store.* calls) │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ lib/store.go │
│ ┌─────────────────────────────────────────────────┐ │
│ │ RBAC Check Layer │ │
│ │ - Check accessor permissions before query │ │
│ │ - Filter results based on access grants │ │
│ │ - Cache permissions per accessor-dossier pair │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Data Layer │ │
│ │ - Auto-encrypt on write │ │
│ │ - Auto-decrypt on read │ │
│ │ - Reflection-based struct mapping │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
┌─────────────┐
│ SQLite │
└─────────────┘
```
## Migration Phases
### Phase 1: Consolidate lib/ (Week 1)
Move all SQL from lib/*.go into store.go patterns.
**1.1 - lib/data.go Entry functions → store.go**
- [ ] `EntryAdd()``store.Insert(entry, accessorID)`
- [ ] `EntryUpdate()``store.Update(entry, accessorID)`
- [ ] `EntryDelete()``store.Delete(entry, accessorID)`
- [ ] `EntryGet()``store.Get(entry, accessorID)`
- [ ] `EntryList()``store.List(filter, accessorID)`
- [ ] `EntryAddBatch()``store.InsertBatch(entries, accessorID)`
**1.2 - lib/data.go Dossier functions → store.go**
- [ ] `DossierWrite()``store.Insert/Update(dossier, accessorID)`
- [ ] `DossierGet()``store.Get(dossier, accessorID)`
- [ ] `DossierGetByEmail()``store.GetBy(dossier, "email", accessorID)`
- [ ] `DossierList()``store.List(filter, accessorID)`
**1.3 - lib/data.go Access functions → store.go**
- [ ] `AccessWrite()``store.GrantAccess(accessor, target, perms)`
- [ ] `AccessList()``store.ListAccess(dossierID)`
- [ ] `AccessRevoke()``store.RevokeAccess(accessor, target)`
**1.4 - lib/prompt.go → store.go**
- [ ] Move prompt CRUD to store patterns
- [ ] Prompts are user-scoped, add accessor checks
**1.5 - lib/db.go**
- [ ] Keep DBInit/DBClose/DB()
- [ ] Remove DBExec, DBInsert, DBUpdate, DBDelete, DBQuery, DBQueryRow
- [ ] These become internal to store.go only
### Phase 2: Consolidate api/ (Week 2)
Replace raw SQL in API handlers with store calls.
**2.1 - api/api_access.go (5 ops)**
- [ ] SELECT dossier_access → `store.ListAccess()`
- [ ] INSERT dossier_access → `store.GrantAccess()`
- [ ] UPDATE accessed_at → `store.TouchAccess()`
- [ ] DELETE dossier_access → `store.RevokeAccess()`
**2.2 - api/api_genome.go (4 ops)**
- [ ] Complex JOIN queries need special handling
- [ ] Option A: Add `store.QueryGenome(filter, accessorID)` specialized method
- [ ] Option B: Build query in store, return filtered results
- [ ] Must preserve tier-matching logic
**2.3 - api/api_categories.go (7 ops)**
- [ ] PRAGMA queries for schema info - keep as utility
- [ ] Category listing → `store.ListCategories()`
**2.4 - api/api_dossier.go (4 ops)**
- [ ] All should route through `store.Get/Update(dossier, accessorID)`
### Phase 3: Consolidate portal/ (Week 2)
**3.1 - portal/main.go (6 ops)**
- [ ] Session table: CREATE IF NOT EXISTS - keep in init
- [ ] Rate limiting: separate concern, could stay separate or move to store
- [ ] Session cleanup: utility function
**3.2 - Unify db connection**
- [ ] Portal currently opens its own connection
- [ ] Should use lib.DB() like API does
- [ ] Single connection pool for both
### Phase 4: Add RBAC Layer (Week 3)
Once all access goes through store.go:
**4.1 - Permission cache**
```go
type permCache struct {
mu sync.RWMutex
cache map[string]*permEntry // key: "accessor:target"
ttl time.Duration
}
type permEntry struct {
perms uint8 // rwdm bitmask
expires time.Time
}
```
**4.2 - Check functions**
```go
func (s *Store) canRead(accessorID, targetDossierID, entryID string) bool
func (s *Store) canWrite(accessorID, targetDossierID, entryID string) bool
func (s *Store) canDelete(accessorID, targetDossierID, entryID string) bool
func (s *Store) canManage(accessorID, targetDossierID string) bool
```
**4.3 - Integrate checks**
- Every store.Get/List filters by accessor permissions
- Every store.Insert/Update checks write permission
- Every store.Delete checks delete permission
- Access grants require manage permission
## Store API Design
```go
// Context carries accessor identity
type StoreContext struct {
AccessorID string // who is making the request
BypassRBAC bool // for system operations only
}
// Main store interface
func Get[T any](ctx StoreContext, id string) (*T, error)
func List[T any](ctx StoreContext, filter Filter) ([]*T, error)
func Insert[T any](ctx StoreContext, item *T) error
func Update[T any](ctx StoreContext, item *T) error
func Delete[T any](ctx StoreContext, id string) error
// Access-specific
func GrantAccess(ctx StoreContext, grant AccessGrant) error
func RevokeAccess(ctx StoreContext, accessor, target string) error
func ListAccess(ctx StoreContext, dossierID string) ([]AccessGrant, error)
```
## Files to Modify
### High Priority (direct SQL)
1. `lib/data.go` - Refactor 26 functions to use store internally
2. `lib/prompt.go` - Refactor 8 functions
3. `api/api_access.go` - Replace 5 raw queries
4. `api/api_genome.go` - Replace 4 raw queries
5. `api/api_dossier.go` - Replace 4 raw queries
### Medium Priority
6. `api/api_categories.go` - 7 queries (some are PRAGMA)
7. `portal/main.go` - 6 queries (sessions, rate limiting)
8. `lib/db.go` - Remove public query helpers
### Low Priority (already clean)
- `api/api_entries.go` - Uses lib functions
- `api/api_v1.go` - Uses lib functions
- `api/api_studies.go` - Uses lib functions
- etc.
## Migration Strategy
1. **Don't break existing code** - Keep old function signatures, change internals
2. **Add accessor parameter gradually** - Start with optional, make required later
3. **Test each phase** - Deploy after each phase, verify nothing breaks
4. **RBAC off by default** - Add checks but disable until fully migrated
## Success Criteria
- [ ] Zero `db.Query/Exec` calls outside lib/store.go
- [ ] All data access includes accessor identity
- [ ] Permission cache working with TTL
- [ ] RBAC checks enforced at store layer
- [ ] Audit log captures all access attempts