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.
|
||||
/// 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)
|
||||
|
|
|
|||
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
|
||||
|
||||
- [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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue