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>
119 lines
4.4 KiB
PL/PgSQL
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;
|