Files
triqura-ecd/docs/design/RLS_SECURITY.md
2025-11-23 10:13:00 +01:00

305 lines
8.4 KiB
Markdown

# 🔒 Row Level Security (RLS) Documentation
**Project:** AI Speedrun - Mini-ECD Prototype
**Epic:** E2 - Database & Auth
**Story:** E2.S2 - RLS policies implementeren
**Last Updated:** 2024-11-15
---
## Overview
This document describes the Row Level Security (RLS) implementation for the EPD core database tables. RLS is PostgreSQL's security feature that restricts which rows users can access in database queries.
### Security Model
- **Authentication Required:** All data access requires a valid Supabase authentication session
- **Authorization:** Checked via `auth.uid()` function which returns the authenticated user's UUID
- **MVP Level:** All authenticated users can access all data (suitable for demo/single-org)
- **Production Path:** Ready to extend with `org_id` filtering for multi-tenancy
---
## Tables & Policies
### 1. Clients Table
**Purpose:** Basic client information
**RLS Enabled:** ✅ Yes
#### Policies:
| Policy Name | Operation | Rule |
|------------|-----------|------|
| Authenticated users can view clients | SELECT | `auth.uid() IS NOT NULL` |
| Authenticated users can create clients | INSERT | `auth.uid() IS NOT NULL` |
| Authenticated users can update clients | UPDATE | `auth.uid() IS NOT NULL` |
| Authenticated users can delete clients | DELETE | `auth.uid() IS NOT NULL` |
**Production Enhancement:**
```sql
-- Add organization filtering
CREATE POLICY "Users can view own org clients"
ON clients FOR SELECT
USING (
auth.uid() IS NOT NULL AND
org_id = (SELECT org_id FROM users WHERE id = auth.uid())
);
```
---
### 2. Intake Notes Table
**Purpose:** TipTap/ProseMirror JSON content storage
**RLS Enabled:** ✅ Yes
#### Policies:
| Policy Name | Operation | Rule |
|------------|-----------|------|
| Authenticated users can view intake notes | SELECT | `auth.uid() IS NOT NULL` |
| Authenticated users can create intake notes | INSERT | `auth.uid() IS NOT NULL` |
| Authenticated users can update intake notes | UPDATE | `auth.uid() IS NOT NULL` |
| Authenticated users can delete intake notes | DELETE | `auth.uid() IS NOT NULL` |
**Security Features:**
- Full-text search index with Dutch language support
- Cascade delete when parent client is deleted
- Automatic `updated_at` trigger
---
### 3. Problem Profiles Table
**Purpose:** DSM-light categorization with severity scoring
**RLS Enabled:** ✅ Yes
#### Policies:
| Policy Name | Operation | Rule |
|------------|-----------|------|
| Authenticated users can view problem profiles | SELECT | `auth.uid() IS NOT NULL` |
| Authenticated users can create problem profiles | INSERT | `auth.uid() IS NOT NULL` |
| Authenticated users can update problem profiles | UPDATE | `auth.uid() IS NOT NULL` |
| Authenticated users can delete problem profiles | DELETE | `auth.uid() IS NOT NULL` |
**Data Constraints:**
- Category: Must be one of 6 DSM-light categories
- Severity: Must be 'laag', 'middel', or 'hoog'
- Cascade delete with parent client
- SET NULL on source note deletion
---
### 4. Treatment Plans Table
**Purpose:** Treatment plans with JSONB structure and versioning
**RLS Enabled:** ✅ Yes
#### Policies:
| Policy Name | Operation | Rule |
|------------|-----------|------|
| Authenticated users can view treatment plans | SELECT | `auth.uid() IS NOT NULL` |
| Authenticated users can create treatment plans | INSERT | `auth.uid() IS NOT NULL` |
| Authenticated users can update treatment plans | UPDATE | `auth.uid() IS NOT NULL` |
| Authenticated users can delete treatment plans | DELETE | `auth.uid() IS NOT NULL` |
**Versioning:**
- Each client can have multiple versions (v1, v2, etc.)
- Status: 'concept' (editable) or 'gepubliceerd' (locked)
- UNIQUE constraint on (client_id, version)
---
### 5. AI Events Table
**Purpose:** Telemetry and debugging for AI API calls
**RLS Enabled:** ✅ Yes
**Special:** Append-only (no UPDATE/DELETE for regular users)
#### Policies:
| Policy Name | Operation | Rule |
|------------|-----------|------|
| Authenticated users can view AI events | SELECT | `auth.uid() IS NOT NULL` |
| Authenticated users can create AI events | INSERT | `auth.uid() IS NOT NULL` |
| ~~UPDATE~~ | ❌ | Not allowed (audit trail) |
| ~~DELETE~~ | ❌ | Not allowed (audit trail) |
**Immutability:**
- Regular users cannot modify or delete AI events
- Ensures audit trail integrity
- Service role can bypass RLS for admin cleanup
---
## Testing RLS
### Test 1: Verify RLS is Enabled
```sql
SELECT tablename, rowsecurity as rls_enabled
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY tablename;
```
**Expected Result:**
All 5 tables should show `rls_enabled: true`
### Test 2: Check Policy Count
```sql
SELECT
tablename,
COUNT(*) as policy_count,
STRING_AGG(cmd, ', ' ORDER BY cmd) as commands
FROM pg_policies
WHERE schemaname = 'public'
GROUP BY tablename;
```
**Expected Result:**
- `ai_events`: 2 policies (INSERT, SELECT)
- Other tables: 4 policies each (DELETE, INSERT, SELECT, UPDATE)
### Test 3: Verify Authentication Check
```sql
SELECT tablename, policyname, cmd, qual
FROM pg_policies
WHERE schemaname = 'public'
AND qual NOT LIKE '%auth.uid()%';
```
**Expected Result:**
Empty (all policies use `auth.uid()` checks)
---
## TypeScript Integration
TypeScript types are auto-generated and available at `lib/database.types.ts`:
```typescript
import type { Database } from '@/lib/database.types'
// Usage with Supabase client
const supabase = createClient<Database>(url, key)
// Type-safe queries
const { data: clients } = await supabase
.from('clients')
.select('*')
// Insert with type checking
const { data: newClient } = await supabase
.from('clients')
.insert({
first_name: 'John',
last_name: 'Doe',
birth_date: '1990-01-01'
})
```
---
## Security Best Practices
### ✅ Current Implementation
1. **Secure by Default:** RLS enabled on all tables
2. **Authentication Required:** All policies check `auth.uid() IS NOT NULL`
3. **Separation of Concerns:** Separate policies for each operation (SELECT, INSERT, UPDATE, DELETE)
4. **Audit Trail:** AI events are append-only
5. **Foreign Key Constraints:** Automatic cleanup with CASCADE/SET NULL
6. **Type Safety:** Generated TypeScript types prevent runtime errors
### 🔄 Production Enhancements
When moving to production with multiple organizations:
1. **Add Organization Column:**
```sql
ALTER TABLE clients ADD COLUMN org_id UUID REFERENCES organizations(id);
```
2. **Update Policies with Org Filtering:**
```sql
CREATE POLICY "Users can view own org data"
ON clients FOR SELECT
USING (
auth.uid() IS NOT NULL AND
org_id = (SELECT org_id FROM users WHERE id = auth.uid())
);
```
3. **Add Role-Based Access:**
```sql
CREATE POLICY "Admins can view all"
ON clients FOR SELECT
USING (
auth.uid() IS NOT NULL AND
EXISTS (
SELECT 1 FROM users
WHERE id = auth.uid() AND role IN ('admin', 'superadmin')
)
);
```
4. **Implement Row-Level Ownership:**
```sql
CREATE POLICY "Users can update own records"
ON intake_notes FOR UPDATE
USING (author = auth.uid());
```
---
## Troubleshooting
### Issue: "new row violates row-level security policy"
**Cause:** Trying to insert/update data that doesn't satisfy RLS WITH CHECK
**Solution:** Ensure user is authenticated and data meets policy requirements
### Issue: No data returned despite existing rows
**Cause:** User not authenticated or RLS USING clause filters out all rows
**Solution:** Verify `auth.uid()` returns a valid UUID
### Issue: Service role queries still restricted
**Cause:** Using anon key instead of service role key
**Solution:** Use `SUPABASE_SERVICE_ROLE_KEY` for admin operations
```typescript
// Service role bypasses RLS
const supabase = createClient(url, serviceRoleKey)
```
---
## Migration History
| Migration | Date | Changes |
|-----------|------|---------|
| `20241115000002_create_epd_core_tables.sql` | 2024-11-15 | Initial RLS policies (demo-level) |
| `20241115000003_enhance_rls_policies.sql` | 2024-11-15 | Granular policies per operation + ai_events immutability |
---
## References
- [Supabase RLS Documentation](https://supabase.com/docs/guides/auth/row-level-security)
- [PostgreSQL RLS Documentation](https://www.postgresql.org/docs/current/ddl-rowsecurity.html)
- Technical Design: `docs/specs/to-mini-ecd-v1_2.md` § 2.4
- Build Plan: `docs/specs/bouwplan-ai-speedrun-marketing-first-v1.1.md` Epic 2
---
**Status:** ✅ Implemented and Tested
**Next Steps:** E2.S3 - Demo auth flow