diff --git a/maskinrommet/src/intentions.rs b/maskinrommet/src/intentions.rs index cb6e596..290609b 100644 --- a/maskinrommet/src/intentions.rs +++ b/maskinrommet/src/intentions.rs @@ -65,12 +65,70 @@ fn stdb_error(op: &str, e: crate::stdb::StdbError) -> (StatusCode, Json Result { + // Finn brukerens alias som er deltaker i kommunikasjonsnoden. + // Alias-edge: user_id --alias(system=true)--> alias_id + // Deltaker-edge: alias_id --owner/member_of/host_of--> context_id + let alias_id = sqlx::query_scalar::<_, Uuid>( + r#" + SELECT e_alias.target_id + FROM edges e_alias + JOIN edges e_participant + ON e_participant.source_id = e_alias.target_id + WHERE e_alias.source_id = $1 + AND e_alias.edge_type = 'alias' + AND e_alias.system = true + AND e_participant.target_id = $2 + AND e_participant.edge_type IN ('owner', 'member_of', 'host_of') + LIMIT 1 + "#, + ) + .bind(user_id) + .bind(context_id) + .fetch_optional(db) + .await?; + + Ok(alias_id.unwrap_or(user_id)) +} + +/// Henter alle alias-IDer for en bruker (via system alias-edges). +#[allow(dead_code)] +async fn user_alias_ids(db: &PgPool, user_id: Uuid) -> Result, sqlx::Error> { + let ids = sqlx::query_scalar::<_, Uuid>( + r#" + SELECT target_id FROM edges + WHERE source_id = $1 AND edge_type = 'alias' AND system = true + "#, + ) + .bind(user_id) + .fetch_all(db) + .await?; + + Ok(ids) +} + /// Sjekker om brukeren har skrivetilgang til en node. -/// Returnerer true hvis brukeren er created_by, eller har owner/admin-edge. +/// Returnerer true hvis brukeren (eller et av brukerens aliaser) er created_by, +/// eller har owner/admin-edge til noden. async fn user_can_modify_node(db: &PgPool, user_id: Uuid, node_id: Uuid) -> Result { + // Sjekk direkte eierskap, alias-eierskap, eller admin/owner-edge let row = sqlx::query_scalar::<_, bool>( r#" SELECT EXISTS( @@ -79,6 +137,14 @@ async fn user_can_modify_node(db: &PgPool, user_id: Uuid, node_id: Uuid) -> Resu SELECT 1 FROM edges WHERE source_id = $2 AND target_id = $1 AND edge_type IN ('owner', 'admin') + ) OR EXISTS( + -- Sjekk om created_by er et av brukerens aliaser + SELECT 1 FROM nodes n + JOIN edges e_alias ON e_alias.target_id = n.created_by + WHERE n.id = $1 + AND e_alias.source_id = $2 + AND e_alias.edge_type = 'alias' + AND e_alias.system = true ) "#, ) @@ -91,7 +157,8 @@ async fn user_can_modify_node(db: &PgPool, user_id: Uuid, node_id: Uuid) -> Resu } /// Sjekker om brukeren har skrivetilgang til en edge. -/// Brukeren må ha opprettet edgen, eller ha owner/admin-edge til source-noden. +/// Brukeren må ha opprettet edgen (direkte eller via alias), +/// eller ha owner/admin-edge til source-noden. #[allow(dead_code)] async fn user_can_modify_edge(db: &PgPool, user_id: Uuid, edge_id: Uuid) -> Result { let row = sqlx::query_scalar::<_, bool>( @@ -104,6 +171,14 @@ async fn user_can_modify_edge(db: &PgPool, user_id: Uuid, edge_id: Uuid) -> Resu AND access_edge.target_id = e.source_id AND access_edge.edge_type IN ('owner', 'admin') WHERE e.id = $1 + ) OR EXISTS( + -- Sjekk om created_by er et av brukerens aliaser + SELECT 1 FROM edges e + JOIN edges e_alias ON e_alias.target_id = e.created_by + WHERE e.id = $1 + AND e_alias.source_id = $2 + AND e_alias.edge_type = 'alias' + AND e_alias.system = true ) "#, ) @@ -211,10 +286,24 @@ pub async fn create_node( .unwrap_or_else(|| serde_json::json!({})); let metadata_str = metadata.to_string(); + // -- Kontekstbasert identitet (oppgave 8.2) -- + // Hvis context_id er satt, sjekk om brukeren har et alias som er + // deltaker i kommunikasjonsnoden. I så fall brukes aliaset som created_by. + let effective_identity = if let Some(ctx_id) = req.context_id { + resolve_context_identity(&state.db, user.node_id, ctx_id) + .await + .map_err(|e| { + tracing::error!("PG-feil ved identitetsoppslag: {e}"); + internal_error("Databasefeil ved identitetsoppslag") + })? + } else { + user.node_id + }; + // -- Generer UUIDv7 (tidssortert) -- let node_id = Uuid::now_v7(); let node_id_str = node_id.to_string(); - let created_by_str = user.node_id.to_string(); + let created_by_str = effective_identity.to_string(); // -- Skriv til SpacetimeDB (instant) -- state @@ -234,8 +323,10 @@ pub async fn create_node( tracing::info!( node_id = %node_id, node_kind = %node_kind, - created_by = %user.node_id, + created_by = %effective_identity, + auth_user = %user.node_id, context_id = ?req.context_id, + alias_used = %(effective_identity != user.node_id), "Node opprettet i STDB" ); @@ -248,7 +339,7 @@ pub async fn create_node( content, visibility, metadata, - user.node_id, + effective_identity, ); // -- Kontekst-arv: automatisk belongs_to-edge -- @@ -290,7 +381,7 @@ pub async fn create_node( "belongs_to".to_string(), bt_metadata, false, - user.node_id, + effective_identity, ); Some(edge_id) diff --git a/migrations/006_alias_aware_rls.sql b/migrations/006_alias_aware_rls.sql new file mode 100644 index 0000000..223ea2a --- /dev/null +++ b/migrations/006_alias_aware_rls.sql @@ -0,0 +1,79 @@ +-- 006_alias_aware_rls.sql +-- Oppdaterer RLS-policies til å håndtere alias-basert created_by. +-- +-- Når en bruker opererer via et alias (oppgave 8.2), settes created_by +-- til alias-noden i stedet for brukerens hovednode. RLS-policies må +-- da gjenkjenne at brukeren eier sine aliaser og dermed har tilgang +-- til noder/edges opprettet av aliaset. +-- +-- Ref: docs/primitiver/nodes.md (created_by), docs/primitiver/edges.md (alias) + +BEGIN; + +-- ============================================================================= +-- Hjelpefunksjon: henter brukerens alias-IDer +-- ============================================================================= + +CREATE OR REPLACE FUNCTION current_node_alias_ids() RETURNS SETOF UUID AS $$ +BEGIN + RETURN QUERY + SELECT target_id FROM edges + WHERE source_id = current_node_id() + AND edge_type = 'alias' + AND system = true; +END; +$$ LANGUAGE plpgsql STABLE; + +GRANT EXECUTE ON FUNCTION current_node_alias_ids() TO synops_reader; + +-- ============================================================================= +-- Oppdatert RLS på nodes — inkluderer alias-eierskap +-- ============================================================================= + +DROP POLICY IF EXISTS node_select ON nodes; + +CREATE POLICY node_select ON nodes FOR SELECT TO synops_reader + USING ( + -- Egne noder (direkte created_by) + created_by = current_node_id() + -- Noder opprettet av et av brukerens aliaser + OR created_by IN (SELECT current_node_alias_ids()) + -- Eksplisitt tilgang via node_access + OR id IN ( + SELECT object_id FROM node_access + WHERE subject_id = current_node_id() + ) + -- Offentlig synlige noder + OR visibility >= 'discoverable' + ); + +-- ============================================================================= +-- Oppdatert RLS på edges — inkluderer alias-eierskap +-- ============================================================================= + +DROP POLICY IF EXISTS edge_select ON edges; + +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 (direkte eller via alias) + created_by = current_node_id() + OR created_by IN (SELECT current_node_alias_ids()) + -- 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() + ) + ) + ); + +COMMIT; diff --git a/tasks.md b/tasks.md index bc14ff3..15fea20 100644 --- a/tasks.md +++ b/tasks.md @@ -105,8 +105,7 @@ Uavhengige faser kan fortsatt plukkes. ## Fase 8: Aliaser - [x] 8.1 Alias-noder: opprett alias-node med `alias`-edge (system=true) fra hovednoden. Usynlig for traversering. -- [~] 8.2 Kontekstbasert identitet: maskinrommet setter `created_by` til alias-node når brukeren opererer i kontekst der aliaset er vert/deltaker. - > Påbegynt: 2026-03-17T19:10 +- [x] 8.2 Kontekstbasert identitet: maskinrommet setter `created_by` til alias-node når brukeren opererer i kontekst der aliaset er vert/deltaker. ## Fase 9: Flere visninger