From 1355d189b2b0768faea5d023f40ca601393aafb3 Mon Sep 17 00:00:00 2001 From: vegard Date: Tue, 17 Mar 2026 15:30:29 +0100 Subject: [PATCH] =?UTF-8?q?Fullf=C3=B8r=20oppgave=204.4:=20RLS-policies=20?= =?UTF-8?q?p=C3=A5=20PG=20med=20node=5Faccess-filtrering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/setup/migration_safety.md | 289 +++++++++++++++----------------- maskinrommet/src/intentions.rs | 2 +- maskinrommet/src/main.rs | 2 + maskinrommet/src/queries.rs | 202 ++++++++++++++++++++++ migrations/004_rls_policies.sql | 119 +++++++++++++ tasks.md | 3 +- 6 files changed, 457 insertions(+), 160 deletions(-) create mode 100644 maskinrommet/src/queries.rs create mode 100644 migrations/004_rls_policies.sql diff --git a/docs/setup/migration_safety.md b/docs/setup/migration_safety.md index abf8fe0..f5d1a57 100644 --- a/docs/setup/migration_safety.md +++ b/docs/setup/migration_safety.md @@ -1,157 +1,132 @@ -# Migration Safety Checklist - -> **Merk:** Denne sjekklisten er skrevet for v1-arkitekturen der RLS var -> basert på `workspace_id`-kolonner og `SET app.current_workspace_id`. -> Workspace-modellen er erstattet av en node-basert tilgangsmatrise (se -> `docs/retninger/bruker_ikke_workspace.md`). Sjekklisten må skrives om for -> det nye mønsteret: `node_access`-matrise, edge-basert tilgang, og -> RLS-policies som opererer på bruker→node-edges i stedet for workspace-scope. -> -> Seksjonene under er bevart som referanse for v1-mønsteret. - -Sjekkliste for alle som kjører PostgreSQL-migrasjoner — lokalt eller i prod. - -## Før migrering - -- [ ] Les migrasjonfilen og forstå hva den gjør -- [ ] Har migrasjonen en tilhørende **down-migrering**? (påkrevd for skjema-endringer) -- [ ] Tar migrasjonen backup-hensyn? (dropper den kolonner/tabeller med data?) - -## Etter migrering - -### RLS-verifisering (KRITISK) -Etter *enhver* migrering som oppretter eller endrer tabeller med `workspace_id`: - -```sql --- 1. Verifiser at RLS er aktivert på alle workspace-tabeller -SELECT tablename, rowsecurity -FROM pg_tables -WHERE schemaname = 'public' - AND tablename IN ('nodes', 'graph_edges', 'messages', 'channels', - 'media_files', 'job_queue', 'message_attachments') -ORDER BY tablename; --- Forventet: rowsecurity = true for alle - --- 2. Verifiser at policies eksisterer -SELECT tablename, policyname, cmd, qual -FROM pg_policies -WHERE schemaname = 'public' -ORDER BY tablename; --- Forventet: workspace_isolation_* policy for hver tabell - --- 3. Test isolasjon: sett workspace A, forsøk å lese workspace B -SET app.current_workspace_id = ''; -SELECT count(*) FROM nodes WHERE workspace_id = ''; --- Forventet: 0 rader (RLS blokkerer) - --- 4. Verifiser at superuser IKKE blokkeres (Rust workers trenger dette) -RESET app.current_workspace_id; -SET ROLE postgres; -SELECT count(*) FROM nodes; --- Forventet: alle rader synlige -``` - -### Indeksverifisering -```sql --- Sjekk at viktige indekser finnes -SELECT indexname, tablename -FROM pg_indexes -WHERE schemaname = 'public' - AND tablename IN ('nodes', 'graph_edges', 'messages') -ORDER BY tablename; -``` - -### Constraint-verifisering -```sql --- Sjekk at foreign keys er intakte -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) - -`SET app.current_workspace_id` er en skjult single point of failure — en glemt SET i en ny feature, en feil i connection-pool, eller en ny tjeneste som kobler til PG uten middleware kan føre til cross-workspace datalekkasje. Denne testen fanger det opp. - -### Automatisk CI-test (to-workspace leak detection) -Kjøres i migrasjonstester og som egen CI-steg: - -```sql --- Opprett to test-workspaces -INSERT INTO workspaces (id, name, slug) VALUES - ('aaaaaaaa-0000-0000-0000-000000000001', 'Workspace A', 'ws-a'), - ('aaaaaaaa-0000-0000-0000-000000000002', 'Workspace B', 'ws-b'); - --- Seed testdata i begge -INSERT INTO nodes (id, node_type, workspace_id) VALUES - ('bbbbbbbb-0000-0000-0000-000000000001', 'tema', 'aaaaaaaa-0000-0000-0000-000000000001'), - ('bbbbbbbb-0000-0000-0000-000000000002', 'tema', 'aaaaaaaa-0000-0000-0000-000000000002'); - --- TEST 1: Sett workspace A, forsøk å lese workspace B -SET app.current_workspace_id = 'aaaaaaaa-0000-0000-0000-000000000001'; -DO $$ -BEGIN - IF (SELECT count(*) FROM nodes WHERE workspace_id = 'aaaaaaaa-0000-0000-0000-000000000002') > 0 THEN - RAISE EXCEPTION 'RLS LEAK: Workspace A kan lese Workspace B sine noder!'; - END IF; -END $$; - --- TEST 2: Uten SET (tom current_setting) skal returnere 0 rader -RESET app.current_workspace_id; -DO $$ -BEGIN - -- For vanlig bruker (ikke superuser) bør dette returnere 0 - IF (SELECT count(*) FROM nodes) > 0 AND current_setting('is_superuser') = 'off' THEN - RAISE EXCEPTION 'RLS LEAK: Uautentisert tilkobling kan lese data!'; - END IF; -END $$; -``` - -### Audit-trigger (produksjon) -Valgfri trigger som logger mistenkelige queries i prod: - -```sql --- Tabell for RLS-audit -CREATE TABLE IF NOT EXISTS rls_audit_log ( - id BIGSERIAL PRIMARY KEY, - table_name TEXT NOT NULL, - operation TEXT NOT NULL, - current_workspace TEXT, - session_user TEXT NOT NULL, - query_timestamp TIMESTAMPTZ NOT NULL DEFAULT now() -); - --- Funksjon som logger når current_workspace_id ikke er satt -CREATE OR REPLACE FUNCTION audit_rls_context() RETURNS TRIGGER AS $$ -BEGIN - IF current_setting('app.current_workspace_id', true) IS NULL - OR current_setting('app.current_workspace_id', true) = '' THEN - IF current_setting('is_superuser') = 'off' THEN - INSERT INTO rls_audit_log (table_name, operation, current_workspace, session_user) - VALUES (TG_TABLE_NAME, TG_OP, current_setting('app.current_workspace_id', true), session_user); - END IF; - END IF; - RETURN NEW; -END; -$$ LANGUAGE plpgsql; -``` - -**Kjør leak hunter mot ALLE tabeller med workspace_id — ikke bare de som er listet over.** Nye tabeller legges til i listen automatisk via introspeksjon: - -```sql --- Finn alle tabeller med workspace_id-kolonne (bør alle ha RLS) -SELECT t.tablename -FROM pg_tables t -JOIN information_schema.columns c ON c.table_name = t.tablename -WHERE c.column_name = 'workspace_id' - AND t.schemaname = 'public' - AND NOT EXISTS ( - SELECT 1 FROM pg_policies p WHERE p.tablename = t.tablename - ); --- Forventet: 0 rader. Enhver rad her = tabell med workspace_id UTEN RLS-policy. -``` - -## Automatisering -Disse sjekkene kjøres automatisk i migrasjonstestene (se `docs/arkitektur.md` §10.2). Manuell kjøring er kun nødvendig ved prod-migrasjoner til automatiserte tester er på plass. **RLS Leak Hunter bør prioriteres som første CI-steg — den beskytter mot den mest alvorlige feilkategorien (cross-workspace datalekkasje).** +# 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 = ''; +SET LOCAL ROLE synops_reader; +SELECT count(*) FROM nodes WHERE created_by = '' 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 diff --git a/maskinrommet/src/intentions.rs b/maskinrommet/src/intentions.rs index 6c801d9..ad60ceb 100644 --- a/maskinrommet/src/intentions.rs +++ b/maskinrommet/src/intentions.rs @@ -26,7 +26,7 @@ const VALID_VISIBILITIES: &[&str] = &["hidden", "discoverable", "readable", "ope #[derive(Serialize)] pub struct ErrorResponse { - error: String, + pub error: String, } fn bad_request(msg: &str) -> (StatusCode, Json) { diff --git a/maskinrommet/src/main.rs b/maskinrommet/src/main.rs index a6b7e9d..500c98a 100644 --- a/maskinrommet/src/main.rs +++ b/maskinrommet/src/main.rs @@ -1,5 +1,6 @@ mod auth; mod intentions; +mod queries; mod stdb; mod warmup; @@ -117,6 +118,7 @@ async fn main() { .route("/intentions/create_edge", post(intentions::create_edge)) .route("/intentions/update_node", post(intentions::update_node)) .route("/intentions/delete_node", post(intentions::delete_node)) + .route("/query/nodes", get(queries::query_nodes)) .layer(TraceLayer::new_for_http()) .with_state(state); diff --git a/maskinrommet/src/queries.rs b/maskinrommet/src/queries.rs new file mode 100644 index 0000000..66223e7 --- /dev/null +++ b/maskinrommet/src/queries.rs @@ -0,0 +1,202 @@ +// Tunge spørringer — lesestien via PostgreSQL med RLS. +// +// For søk, statistikk, og graf-traversering brukes PG direkte (ikke STDB). +// Alle spørringer kjøres med SET LOCAL ROLE synops_reader, som er underlagt +// RLS-policies. Brukerens node_id settes som sesjonsvariabel. +// +// Ref: docs/retninger/datalaget.md (tunge spørringer-seksjonen) + +use axum::{extract::State, http::StatusCode, Json}; +use serde::{Deserialize, Serialize}; +use sqlx::PgPool; +use uuid::Uuid; + +use crate::auth::AuthUser; +use crate::AppState; +use crate::intentions::ErrorResponse; + +// ============================================================================= +// RLS-kontekst +// ============================================================================= + +/// Setter opp RLS-kontekst for en transaksjon. +/// Etter dette kallet er alle SELECT-spørringer filtrert via node_access. +/// +/// MÅ kalles innenfor en transaksjon (SET LOCAL gjelder kun innenfor tx). +async fn set_rls_context( + tx: &mut sqlx::Transaction<'_, sqlx::Postgres>, + user_node_id: Uuid, +) -> Result<(), sqlx::Error> { + // Sett brukerens node_id som sesjonsvariabel + sqlx::query(&format!( + "SET LOCAL app.current_node_id = '{}'", + user_node_id + )) + .execute(&mut **tx) + .await?; + + // Bytt til synops_reader-rollen (underlagt RLS) + sqlx::query("SET LOCAL ROLE synops_reader") + .execute(&mut **tx) + .await?; + + Ok(()) +} + +// ============================================================================= +// GET /query/nodes — søk og filtrering av noder +// ============================================================================= + +#[derive(Deserialize)] +pub struct QueryNodesRequest { + /// Fritekst-søk i tittel og innhold. Valgfritt. + pub q: Option, + /// Filtrer på node_kind. Valgfritt. + pub kind: Option, + /// Maks antall resultater. Default: 50. + pub limit: Option, + /// Offset for paginering. Default: 0. + pub offset: Option, +} + +#[derive(Serialize, sqlx::FromRow)] +pub struct QueryNodeResult { + pub id: Uuid, + pub node_kind: String, + pub title: Option, + pub content: Option, + pub visibility: String, + pub metadata: serde_json::Value, + pub created_at: chrono::DateTime, + pub created_by: Option, +} + +#[derive(Serialize)] +pub struct QueryNodesResponse { + pub nodes: Vec, + pub total: i64, +} + +fn internal_error(msg: &str) -> (StatusCode, Json) { + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse { + error: msg.to_string(), + }), + ) +} + +/// GET /query/nodes?q=...&kind=...&limit=...&offset=... +/// +/// Søk og filtrering av noder. Bruker RLS — returnerer kun noder +/// brukeren har tilgang til. +pub async fn query_nodes( + State(state): State, + user: AuthUser, + axum::extract::Query(params): axum::extract::Query, +) -> Result, (StatusCode, Json)> { + let limit = params.limit.unwrap_or(50).min(200); + let offset = params.offset.unwrap_or(0).max(0); + + let result = run_query_nodes(&state.db, user.node_id, ¶ms.q, ¶ms.kind, limit, offset).await; + + match result { + Ok(resp) => Ok(Json(resp)), + Err(e) => { + tracing::error!(error = %e, "query_nodes feilet"); + Err(internal_error("Databasefeil ved søk")) + } + } +} + +async fn run_query_nodes( + db: &PgPool, + user_node_id: Uuid, + q: &Option, + kind: &Option, + limit: i64, + offset: i64, +) -> Result { + let mut tx = db.begin().await?; + set_rls_context(&mut tx, user_node_id).await?; + + // Bygg spørring basert på filtre + let (nodes, total) = if let Some(search) = q.as_deref().filter(|s| !s.is_empty()) { + let search_pattern = format!("%{}%", search.replace('%', "\\%").replace('_', "\\_")); + + let nodes = sqlx::query_as::<_, QueryNodeResult>( + r#" + SELECT id, node_kind, title, content, visibility::text as visibility, + metadata, created_at, created_by + FROM nodes + WHERE (title ILIKE $1 OR content ILIKE $1) + ORDER BY created_at DESC + LIMIT $2 OFFSET $3 + "#, + ) + .bind(&search_pattern) + .bind(limit) + .bind(offset) + .fetch_all(&mut *tx) + .await?; + + let total: (i64,) = sqlx::query_as( + "SELECT COUNT(*) FROM nodes WHERE (title ILIKE $1 OR content ILIKE $1)", + ) + .bind(&search_pattern) + .fetch_one(&mut *tx) + .await?; + + (nodes, total.0) + } else if let Some(kind) = kind.as_deref().filter(|s| !s.is_empty()) { + let nodes = sqlx::query_as::<_, QueryNodeResult>( + r#" + SELECT id, node_kind, title, content, visibility::text as visibility, + metadata, created_at, created_by + FROM nodes + WHERE node_kind = $1 + ORDER BY created_at DESC + LIMIT $2 OFFSET $3 + "#, + ) + .bind(kind) + .bind(limit) + .bind(offset) + .fetch_all(&mut *tx) + .await?; + + let total: (i64,) = sqlx::query_as( + "SELECT COUNT(*) FROM nodes WHERE node_kind = $1", + ) + .bind(kind) + .fetch_one(&mut *tx) + .await?; + + (nodes, total.0) + } else { + let nodes = sqlx::query_as::<_, QueryNodeResult>( + r#" + SELECT id, node_kind, title, content, visibility::text as visibility, + metadata, created_at, created_by + FROM nodes + ORDER BY created_at DESC + LIMIT $1 OFFSET $2 + "#, + ) + .bind(limit) + .bind(offset) + .fetch_all(&mut *tx) + .await?; + + let total: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM nodes") + .fetch_one(&mut *tx) + .await?; + + (nodes, total.0) + }; + + // Transaksjon avsluttes — SET LOCAL tilbakestilles automatisk + tx.commit().await?; + + Ok(QueryNodesResponse { nodes, total }) +} diff --git a/migrations/004_rls_policies.sql b/migrations/004_rls_policies.sql new file mode 100644 index 0000000..d5eb751 --- /dev/null +++ b/migrations/004_rls_policies.sql @@ -0,0 +1,119 @@ +-- 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; diff --git a/tasks.md b/tasks.md index efe99c4..d140be0 100644 --- a/tasks.md +++ b/tasks.md @@ -74,8 +74,7 @@ Uavhengige faser kan fortsatt plukkes. - [x] 4.1 `recompute_access` i maskinrommet: ved edge-endring, oppdater `node_access`-matrisen. Håndter direkte edges (owner, admin, member, reader). - [x] 4.2 Team-transitivitet: member_of-edge til team → arv tilgang fra teamets edges. - [x] 4.3 Visibility-filtrering: STDB-spørringer respekterer visibility-enum. Frontend ser bare noder brukeren har tilgang til. -- [~] 4.4 RLS-policies på PG: `node_access`-basert filtrering for tunge spørringer. - > Påbegynt: 2026-03-17T15:19 +- [x] 4.4 RLS-policies på PG: `node_access`-basert filtrering for tunge spørringer. ## Fase 5: Kommunikasjonsnoder