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:
parent
e7bb6d1f8a
commit
f81c8a96e0
3 changed files with 178 additions and 9 deletions
|
|
@ -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.
|
/// 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> {
|
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>(
|
let row = sqlx::query_scalar::<_, bool>(
|
||||||
r#"
|
r#"
|
||||||
SELECT EXISTS(
|
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
|
SELECT 1 FROM edges
|
||||||
WHERE source_id = $2 AND target_id = $1
|
WHERE source_id = $2 AND target_id = $1
|
||||||
AND edge_type IN ('owner', 'admin')
|
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.
|
/// 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)]
|
#[allow(dead_code)]
|
||||||
async fn user_can_modify_edge(db: &PgPool, user_id: Uuid, edge_id: Uuid) -> Result<bool, sqlx::Error> {
|
async fn user_can_modify_edge(db: &PgPool, user_id: Uuid, edge_id: Uuid) -> Result<bool, sqlx::Error> {
|
||||||
let row = sqlx::query_scalar::<_, bool>(
|
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.target_id = e.source_id
|
||||||
AND access_edge.edge_type IN ('owner', 'admin')
|
AND access_edge.edge_type IN ('owner', 'admin')
|
||||||
WHERE e.id = $1
|
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!({}));
|
.unwrap_or_else(|| serde_json::json!({}));
|
||||||
let metadata_str = metadata.to_string();
|
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) --
|
// -- Generer UUIDv7 (tidssortert) --
|
||||||
let node_id = Uuid::now_v7();
|
let node_id = Uuid::now_v7();
|
||||||
let node_id_str = node_id.to_string();
|
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) --
|
// -- Skriv til SpacetimeDB (instant) --
|
||||||
state
|
state
|
||||||
|
|
@ -234,8 +323,10 @@ pub async fn create_node(
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
node_id = %node_id,
|
node_id = %node_id,
|
||||||
node_kind = %node_kind,
|
node_kind = %node_kind,
|
||||||
created_by = %user.node_id,
|
created_by = %effective_identity,
|
||||||
|
auth_user = %user.node_id,
|
||||||
context_id = ?req.context_id,
|
context_id = ?req.context_id,
|
||||||
|
alias_used = %(effective_identity != user.node_id),
|
||||||
"Node opprettet i STDB"
|
"Node opprettet i STDB"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -248,7 +339,7 @@ pub async fn create_node(
|
||||||
content,
|
content,
|
||||||
visibility,
|
visibility,
|
||||||
metadata,
|
metadata,
|
||||||
user.node_id,
|
effective_identity,
|
||||||
);
|
);
|
||||||
|
|
||||||
// -- Kontekst-arv: automatisk belongs_to-edge --
|
// -- Kontekst-arv: automatisk belongs_to-edge --
|
||||||
|
|
@ -290,7 +381,7 @@ pub async fn create_node(
|
||||||
"belongs_to".to_string(),
|
"belongs_to".to_string(),
|
||||||
bt_metadata,
|
bt_metadata,
|
||||||
false,
|
false,
|
||||||
user.node_id,
|
effective_identity,
|
||||||
);
|
);
|
||||||
|
|
||||||
Some(edge_id)
|
Some(edge_id)
|
||||||
|
|
|
||||||
79
migrations/006_alias_aware_rls.sql
Normal file
79
migrations/006_alias_aware_rls.sql
Normal 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;
|
||||||
3
tasks.md
3
tasks.md
|
|
@ -105,8 +105,7 @@ Uavhengige faser kan fortsatt plukkes.
|
||||||
## Fase 8: Aliaser
|
## Fase 8: Aliaser
|
||||||
|
|
||||||
- [x] 8.1 Alias-noder: opprett alias-node med `alias`-edge (system=true) fra hovednoden. Usynlig for traversering.
|
- [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.
|
- [x] 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
|
|
||||||
|
|
||||||
## Fase 9: Flere visninger
|
## Fase 9: Flere visninger
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue