Fullfør oppgave 4.1: recompute_access ved edge-endring

Når en tilgangsgivende edge (owner, admin, member_of, reader)
opprettes, kalles nå recompute_access() i samme PG-transaksjon
som edge-insertet. Dette sikrer at node_access-matrisen alltid
er oppdatert — ingen vindu med stale tilgang.

Implementasjon:
- edge_type_to_access_level() mapper edge-typer til access_level enum
- insert_edge_with_access() wrapper edge-insert + recompute_access i tx
- Vanlige edges (belongs_to, mentions, etc.) skrives som før (fire-and-forget)

Verifisert med SQL-tester: direkte tilgang + transitiv tilgang via
belongs_to-edges fungerer korrekt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
vegard 2026-03-17 14:51:30 +01:00
parent 03211e59a5
commit c20fc9149b
2 changed files with 107 additions and 22 deletions

View file

@ -554,7 +554,21 @@ fn spawn_pg_insert_node(
});
}
/// Mapper edge_type til access_level for tilgangsgivende edges.
/// Returnerer None for edges som ikke gir tilgang.
fn edge_type_to_access_level(edge_type: &str) -> Option<&'static str> {
match edge_type {
"owner" => Some("owner"),
"admin" => Some("admin"),
"member_of" => Some("member"),
"reader" => Some("reader"),
_ => None,
}
}
/// Spawner en tokio-task som skriver edgen til PostgreSQL i bakgrunnen.
/// For tilgangsgivende edges (owner, admin, member_of, reader) kalles
/// recompute_access i samme transaksjon — ingen vindu med stale tilgang.
fn spawn_pg_insert_edge(
db: PgPool,
edge_id: Uuid,
@ -566,33 +580,105 @@ fn spawn_pg_insert_edge(
created_by: Uuid,
) {
tokio::spawn(async move {
let result = sqlx::query(
r#"
INSERT INTO edges (id, source_id, target_id, edge_type, metadata, system, created_by)
VALUES ($1, $2, $3, $4, $5, $6, $7)
"#,
)
.bind(edge_id)
.bind(source_id)
.bind(target_id)
.bind(&edge_type)
.bind(&metadata)
.bind(system)
.bind(created_by)
.execute(&db)
.await;
let access_level = edge_type_to_access_level(&edge_type);
match result {
Ok(_) => {
tracing::info!(edge_id = %edge_id, "Edge persistert til PostgreSQL");
if let Some(level) = access_level {
// Tilgangsgivende edge: wrap i transaksjon med recompute_access
let result = insert_edge_with_access(&db, edge_id, source_id, target_id, &edge_type, &metadata, system, created_by, level).await;
match result {
Ok(_) => {
tracing::info!(
edge_id = %edge_id,
edge_type = %edge_type,
access_level = %level,
"Edge + node_access persistert til PostgreSQL"
);
}
Err(e) => {
tracing::error!(
edge_id = %edge_id,
error = %e,
"Kunne ikke persistere edge + node_access til PostgreSQL"
);
}
}
Err(e) => {
tracing::error!(edge_id = %edge_id, error = %e, "Kunne ikke persistere edge til PostgreSQL");
} else {
// Vanlig edge uten tilgangspåvirkning
let result = sqlx::query(
r#"
INSERT INTO edges (id, source_id, target_id, edge_type, metadata, system, created_by)
VALUES ($1, $2, $3, $4, $5, $6, $7)
"#,
)
.bind(edge_id)
.bind(source_id)
.bind(target_id)
.bind(&edge_type)
.bind(&metadata)
.bind(system)
.bind(created_by)
.execute(&db)
.await;
match result {
Ok(_) => {
tracing::info!(edge_id = %edge_id, "Edge persistert til PostgreSQL");
}
Err(e) => {
tracing::error!(edge_id = %edge_id, error = %e, "Kunne ikke persistere edge til PostgreSQL");
}
}
}
});
}
/// Inserter en tilgangsgivende edge og oppdaterer node_access i én transaksjon.
/// source_id = subject (bruker/team), target_id = object (noden det gis tilgang til).
async fn insert_edge_with_access(
db: &PgPool,
edge_id: Uuid,
source_id: Uuid,
target_id: Uuid,
edge_type: &str,
metadata: &serde_json::Value,
system: bool,
created_by: Uuid,
access_level: &str,
) -> Result<(), sqlx::Error> {
let mut tx = db.begin().await?;
sqlx::query(
r#"
INSERT INTO edges (id, source_id, target_id, edge_type, metadata, system, created_by)
VALUES ($1, $2, $3, $4, $5, $6, $7)
"#,
)
.bind(edge_id)
.bind(source_id)
.bind(target_id)
.bind(edge_type)
.bind(metadata)
.bind(system)
.bind(created_by)
.execute(&mut *tx)
.await?;
// Kall recompute_access: subject=source_id, object=target_id
sqlx::query(
"SELECT recompute_access($1, $2, $3::access_level, $4)",
)
.bind(source_id)
.bind(target_id)
.bind(access_level)
.bind(edge_id)
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(())
}
/// Spawner en tokio-task som oppdaterer noden i PostgreSQL.
fn spawn_pg_update_node(
db: PgPool,

View file

@ -71,8 +71,7 @@ Uavhengige faser kan fortsatt plukkes.
## Fase 4: Tilgangskontroll
- [~] 4.1 `recompute_access` i maskinrommet: ved edge-endring, oppdater `node_access`-matrisen. Håndter direkte edges (owner, admin, member, reader).
> Påbegynt: 2026-03-17T14:45
- [x] 4.1 `recompute_access` i maskinrommet: ved edge-endring, oppdater `node_access`-matrisen. Håndter direkte edges (owner, admin, member, reader).
- [ ] 4.2 Team-transitivitet: member_of-edge til team → arv tilgang fra teamets edges.
- [ ] 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.