synops/docs/setup/migration_safety.md
vegard 1355d189b2 Fullfør oppgave 4.4: RLS-policies på PG med node_access-filtrering
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>
2026-03-17 15:30:29 +01:00

4.3 KiB

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:

-- 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

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

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)

-- 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