synops/migrations/004_rls_policies.sql
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

119 lines
4.4 KiB
PL/PgSQL

-- 004_rls_policies.sql
-- RLS-policies på nodes og edges, basert på node_access-matrisen.
--
-- Maskinrommet kobler til som superuser (sidelinja) for skriveoperasjoner.
-- For tunge lesespørringer (søk, statistikk, graf-traversering) brukes
-- SET LOCAL ROLE synops_reader, som er underlagt RLS.
--
-- Ref: docs/retninger/bruker_ikke_workspace.md (RLS-policy-spesifikasjon)
BEGIN;
-- =============================================================================
-- Applikasjonsrolle for RLS-filtrerte spørringer
-- =============================================================================
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'synops_reader') THEN
CREATE ROLE synops_reader NOLOGIN;
END IF;
END
$$;
-- Gi lesetilgang til alle relevante tabeller
GRANT SELECT ON nodes, edges, node_access, auth_identities TO synops_reader;
-- Tillat synops_reader å lese sekvenser (for eventuelle funksjoner)
GRANT USAGE ON SCHEMA public TO synops_reader;
-- Sidelinja (superuser) kan bytte til synops_reader
GRANT synops_reader TO sidelinja;
-- =============================================================================
-- current_node_id() — leser brukerens node_id fra sesjonsvariabel
-- =============================================================================
CREATE OR REPLACE FUNCTION current_node_id() RETURNS UUID AS $$
BEGIN
RETURN current_setting('app.current_node_id', true)::UUID;
EXCEPTION
WHEN OTHERS THEN
RETURN NULL;
END;
$$ LANGUAGE plpgsql STABLE;
-- synops_reader må kunne kalle funksjonen
GRANT EXECUTE ON FUNCTION current_node_id() TO synops_reader;
-- =============================================================================
-- RLS på nodes
-- =============================================================================
ALTER TABLE nodes ENABLE ROW LEVEL SECURITY;
-- Policy: bruker kan se noder de har opprettet, har tilgang til, eller som er
-- discoverable/readable/open.
--
-- Tre sjekker (ref: bruker_ikke_workspace.md):
-- 1. Egne noder (created_by) — instant
-- 2. Eksplisitt tilgang via node_access — indeksert lookup
-- 3. Offentlig synlige noder (discoverable+) — kolonne-sjekk
CREATE POLICY node_select ON nodes FOR SELECT TO synops_reader
USING (
created_by = current_node_id()
OR id IN (
SELECT object_id FROM node_access
WHERE subject_id = current_node_id()
)
OR visibility >= 'discoverable'
);
-- =============================================================================
-- RLS på edges
-- =============================================================================
ALTER TABLE edges ENABLE ROW LEVEL SECURITY;
-- Policy: bruker kan se edges der de har tilgang til minst én av endepunktene.
-- System-edges (alias etc.) er alltid skjult med mindre brukeren er source.
CREATE POLICY edge_select ON edges FOR SELECT TO synops_reader
USING (
-- Ikke vis system-edges til andre enn eieren
(NOT system OR source_id = current_node_id())
AND (
-- Bruker opprettet edgen
created_by = current_node_id()
-- Bruker er source eller target
OR source_id = current_node_id()
OR target_id = current_node_id()
-- Bruker har tilgang til source- eller target-noden
OR source_id IN (
SELECT object_id FROM node_access
WHERE subject_id = current_node_id()
)
OR target_id IN (
SELECT object_id FROM node_access
WHERE subject_id = current_node_id()
)
)
);
-- =============================================================================
-- RLS på node_access (brukere kan bare se sine egne tilganger)
-- =============================================================================
ALTER TABLE node_access ENABLE ROW LEVEL SECURITY;
CREATE POLICY na_select ON node_access FOR SELECT TO synops_reader
USING (subject_id = current_node_id());
-- =============================================================================
-- Indeks for RLS-ytelse
-- =============================================================================
-- node_access brukes i subquery med subject_id — allerede indeksert (idx_na_subject).
-- Legg til indeks på object_id for edge-policyen (reverse lookup).
CREATE INDEX IF NOT EXISTS idx_na_object ON node_access (object_id);
COMMIT;