Implementerer Row Level Security for tunge PostgreSQL-spørringer. Maskinrommet skriver som superuser (sidelinja), men leser med SET LOCAL ROLE synops_reader som er underlagt RLS-policies. Endringer: - Migration 004: synops_reader rolle, current_node_id() funksjon, RLS-policies på nodes (created_by/node_access/visibility), edges (endepunkt-tilgang + system-edge-skjuling), og node_access (kun egne rader) - queries.rs: RLS-kontekst-helper (set_rls_context) og GET /query/nodes endepunkt med søk, filtrering og paginering - migration_safety.md: omskrevet fra v1 workspace-RLS til node_access-basert RLS med oppdaterte leak hunter-tester Verifisert på server: hidden noder filtrert for ukjente brukere, synlige for eiere. Edges filtrert tilsvarende. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
132 lines
4.3 KiB
Markdown
132 lines
4.3 KiB
Markdown
# Migration Safety Checklist
|
|
|
|
Sjekkliste for alle som kjører PostgreSQL-migrasjoner — lokalt eller i prod.
|
|
|
|
## RLS-modell
|
|
|
|
Synops bruker `node_access`-matrisen for tilgangskontroll, ikke workspace-scope.
|
|
Maskinrommet kobler til som superuser (`sidelinja`) for skriveoperasjoner.
|
|
For tunge lesespørringer brukes `SET LOCAL ROLE synops_reader`, som er underlagt RLS.
|
|
|
|
Sesjonsvariabel: `app.current_node_id` settes til brukerens node-UUID.
|
|
Funksjon: `current_node_id()` leser denne variabelen.
|
|
|
|
RLS-policies finnes på: `nodes`, `edges`, `node_access`.
|
|
|
|
## Før migrering
|
|
|
|
- [ ] Les migrasjonfilen og forstå hva den gjør
|
|
- [ ] Tar migrasjonen backup-hensyn? (dropper den kolonner/tabeller med data?)
|
|
- [ ] Påvirker den RLS-policies? Sjekk at ingen policy fjernes utilsiktet.
|
|
|
|
## Etter migrering
|
|
|
|
### RLS-verifisering (KRITISK)
|
|
Etter *enhver* migrering som oppretter eller endrer tabeller:
|
|
|
|
```sql
|
|
-- 1. Verifiser at RLS er aktivert på relevante tabeller
|
|
SELECT tablename, rowsecurity
|
|
FROM pg_tables
|
|
WHERE schemaname = 'public'
|
|
AND tablename IN ('nodes', 'edges', 'node_access')
|
|
ORDER BY tablename;
|
|
-- Forventet: rowsecurity = true for alle
|
|
|
|
-- 2. Verifiser at policies eksisterer
|
|
SELECT tablename, policyname, cmd
|
|
FROM pg_policies
|
|
WHERE schemaname = 'public'
|
|
ORDER BY tablename;
|
|
-- Forventet: node_select, edge_select, na_select
|
|
|
|
-- 3. Test isolasjon: sett bruker A, forsøk å lese hidden node opprettet av bruker B
|
|
BEGIN;
|
|
SET LOCAL app.current_node_id = '<bruker_a_uuid>';
|
|
SET LOCAL ROLE synops_reader;
|
|
SELECT count(*) FROM nodes WHERE created_by = '<bruker_b_uuid>' AND visibility = 'hidden';
|
|
-- Forventet: 0 rader (RLS blokkerer)
|
|
COMMIT;
|
|
|
|
-- 4. Verifiser at superuser IKKE blokkeres (maskinrommet skriver som superuser)
|
|
SELECT count(*) FROM nodes;
|
|
-- Forventet: alle rader synlige (superuser bypasser RLS)
|
|
```
|
|
|
|
### Indeksverifisering
|
|
```sql
|
|
SELECT indexname, tablename
|
|
FROM pg_indexes
|
|
WHERE schemaname = 'public'
|
|
AND tablename IN ('nodes', 'edges', 'node_access')
|
|
ORDER BY tablename;
|
|
-- Viktige indekser: idx_na_subject, idx_na_object (for RLS-ytelse)
|
|
```
|
|
|
|
### Constraint-verifisering
|
|
```sql
|
|
SELECT tc.table_name, tc.constraint_name, tc.constraint_type
|
|
FROM information_schema.table_constraints tc
|
|
WHERE tc.table_schema = 'public'
|
|
AND tc.constraint_type IN ('FOREIGN KEY', 'CHECK', 'UNIQUE')
|
|
ORDER BY tc.table_name;
|
|
```
|
|
|
|
## RLS Leak Hunter (CI-test)
|
|
|
|
`app.current_node_id` er en single point of failure — en glemt SET i en ny
|
|
feature eller en direkte PG-tilkobling uten `SET LOCAL ROLE synops_reader`
|
|
kan føre til datalekkasje. Denne testen fanger det opp.
|
|
|
|
### Automatisk test (to-bruker leak detection)
|
|
|
|
```sql
|
|
-- Opprett to testbrukere
|
|
INSERT INTO nodes (id, node_kind, title, visibility, created_by) VALUES
|
|
('aaaaaaaa-0000-0000-0000-000000000001', 'person', 'Test A', 'hidden', NULL),
|
|
('aaaaaaaa-0000-0000-0000-000000000002', 'person', 'Test B', 'hidden', NULL);
|
|
|
|
-- Opprett hidden noder for hver bruker
|
|
INSERT INTO nodes (id, node_kind, title, visibility, created_by) VALUES
|
|
('bbbbbbbb-0000-0000-0000-000000000001', 'content', 'Hemmelighet A', 'hidden',
|
|
'aaaaaaaa-0000-0000-0000-000000000001'),
|
|
('bbbbbbbb-0000-0000-0000-000000000002', 'content', 'Hemmelighet B', 'hidden',
|
|
'aaaaaaaa-0000-0000-0000-000000000002');
|
|
|
|
-- TEST 1: Bruker A skal ikke se Bruker B sine hidden noder
|
|
BEGIN;
|
|
SET LOCAL app.current_node_id = 'aaaaaaaa-0000-0000-0000-000000000001';
|
|
SET LOCAL ROLE synops_reader;
|
|
DO $$
|
|
BEGIN
|
|
IF (SELECT count(*) FROM nodes WHERE id = 'bbbbbbbb-0000-0000-0000-000000000002') > 0 THEN
|
|
RAISE EXCEPTION 'RLS LEAK: Bruker A kan lese Bruker B sine hidden noder!';
|
|
END IF;
|
|
END $$;
|
|
COMMIT;
|
|
|
|
-- TEST 2: Uten satt bruker-id skal hidden noder være usynlige
|
|
BEGIN;
|
|
SET LOCAL ROLE synops_reader;
|
|
DO $$
|
|
BEGIN
|
|
IF (SELECT count(*) FROM nodes WHERE visibility = 'hidden') > 0 THEN
|
|
RAISE EXCEPTION 'RLS LEAK: Uautentisert tilkobling kan lese hidden data!';
|
|
END IF;
|
|
END $$;
|
|
COMMIT;
|
|
|
|
-- Rydd opp
|
|
DELETE FROM nodes WHERE id IN (
|
|
'aaaaaaaa-0000-0000-0000-000000000001', 'aaaaaaaa-0000-0000-0000-000000000002',
|
|
'bbbbbbbb-0000-0000-0000-000000000001', 'bbbbbbbb-0000-0000-0000-000000000002'
|
|
);
|
|
```
|
|
|
|
## Nye tabeller
|
|
|
|
Hvis en ny tabell opprettes som inneholder brukerdata:
|
|
1. Aktiver RLS: `ALTER TABLE ny_tabell ENABLE ROW LEVEL SECURITY;`
|
|
2. Opprett SELECT-policy for `synops_reader` med `current_node_id()`-sjekk
|
|
3. Grant SELECT til `synops_reader`
|
|
4. Kjør leak hunter
|