Fullfør oppgave 8.2: Kontekstbasert identitet med alias

Når en bruker oppretter en node i en kommunikasjonskontekst der
brukerens alias er deltaker (owner/member_of/host_of), settes
created_by til alias-noden i stedet for brukerens hovednode.

Endringer:
- resolve_context_identity(): slår opp brukerens alias i konteksten
- create_node(): bruker alias som created_by når context_id er satt
- user_can_modify_node/edge(): gjenkjenner alias-eierskap ved endring/sletting
- 006_alias_aware_rls.sql: RLS-policies inkluderer alias-opprettede noder
- current_node_alias_ids(): PG-funksjon for alias-oppslag i RLS

Verifisert med integrasjonstest på server:
- Node i alias-kontekst → created_by = alias ✓
- Node uten kontekst → created_by = bruker ✓
- Update/delete av alias-node fungerer for hovedbruker ✓

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
vegard 2026-03-17 19:19:36 +01:00
parent e7bb6d1f8a
commit f81c8a96e0
3 changed files with 178 additions and 9 deletions

View file

@ -65,12 +65,70 @@ fn stdb_error(op: &str, e: crate::stdb::StdbError) -> (StatusCode, Json<ErrorRes
}
// =============================================================================
// Tilgangskontroll
// Tilgangskontroll og kontekstbasert identitet
// =============================================================================
/// Løser brukerens identitet i en kommunikasjonskontekst.
///
/// Hvis brukeren har et alias som er deltaker (owner, member_of, host_of)
/// i den gitte kommunikasjonsnoden, returneres alias-nodens ID.
/// Ellers returneres brukerens hoved-node_id.
///
/// Dette gjør at meldinger i en kommunikasjon automatisk krediteres
/// aliaset — f.eks. "Bjørn" i en podcast-samtale i stedet for "Vegard".
///
/// Ref: docs/primitiver/nodes.md (created_by), docs/primitiver/edges.md (alias)
async fn resolve_context_identity(
db: &PgPool,
user_id: Uuid,
context_id: Uuid,
) -> Result<Uuid, sqlx::Error> {
// 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<Vec<Uuid>, 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<bool, sqlx::Error> {
// 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<bool, sqlx::Error> {
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)

View file

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

View file

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