211 lines
8.6 KiB
Markdown
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
|