synops/docs/retninger/bruker_ikke_workspace.md
vegard e0f30bba27 Fullfør oppgave 4.2: team-transitivitet i recompute_access
Legger til steg 4 i recompute_access: når en bruker melder seg inn i
et team (member_of-edge), arver brukeren all tilgang teamet allerede
har. Tidligere håndterte funksjonen kun retningen "team får ny tilgang
→ propager til eksisterende medlemmer" (steg 3). Nå håndteres begge
retninger:

- Steg 3: Team får tilgang → alle eksisterende medlemmer arver
- Steg 4: Ny bruker melder seg inn → arver teamets eksisterende tilgang

Testet med scenario: Trond → Podcastteamet → Sidelinja → Episode 42.
Trond arver member-tilgang til alle tre noder via team-transitivitet.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 15:01:51 +01:00

337 lines
12 KiB
Markdown

# Noder er sentrum
**Status: Besluttet.**
> Alt er noder. En bruker er en node. Et team er en node. Et møte er
> en node. Sidelinja er en node. Relasjoner mellom dem er edges.
> Tilgangskontroll via materialisert tilgangsmatrise beregnet fra
> edge-grafen.
## Beslutningen
1. **Alt er noder.** Brukere, team, prosjekter, innhold, møter —
alt er rader i `nodes`-tabellen. En bruker er en node som
tilfeldigvis kan logge inn.
2. **Relasjoner er edges.** Vegard → Sidelinja (`owner`).
Trond → Sidelinja (`member`). Møtereferat → møte (`belongs_to`).
3. **Ingen containere.** Hva du ser er summen av dine edges.
4. **Samlings-noder gir struktur** — de er vanlige noder som
fungerer som gravitasjonspunkt.
5. **Privat er default** — en node uten edges til andre er kun din.
6. **Tilgangskontroll via `node_access`-matrise**, oppdatert ved
edge-endring, brukt av RLS ved lesing.
## Visibility
Visibility er en egenskap på noden som definerer hva som gjelder
for alle *uten* eksplisitt edge. Eksplisitte edges overrider alltid
oppover.
```sql
CREATE TYPE visibility AS ENUM ('hidden', 'discoverable', 'readable', 'open');
```
| Nivå | Oppdagbar | Lesbar | Interagerbar |
|------|-----------|--------|-------------|
| `hidden` | Nei | Nei | Nei — kun via eksplisitt edge |
| `discoverable` | Ja (søk/katalog) | Nei | Kontaktforespørsel |
| `readable` | Ja | Ja | Nei — krever edge |
| `open` | Ja | Ja | Ja |
Eksempler:
- Spøkelsesbruker: `hidden` — usynlig, kun invitasjon
- Katalogbruker: `discoverable` — finnes i søk, profil krever edge
- Podcast-episode: `readable` — alle kan lytte, bare teamet kan redigere
- Kunnskapsgraf-entitet: `open` — alle kan se og bidra
### Traverseringsregelen
**Visibility er en hard grense ved traversering.** Når du følger
edges i grafen, kan du bare se noder hvis deres visibility tillater
det — eller du har en eksplisitt edge.
Eksempel: Episode #42 (`readable`) har en `host`-edge til Peter
(`hidden`). Anonym bruker leser episoden, følger `host`-edge →
Peter er `hidden`**stopp, usynlig.** Trond (som har edge til
Peter) følger samme edge → **ser Peter.**
Ingen transitivitet bryter denne regelen. Uansett hvor mange
offentlige noder som har edges til en `hidden` node, forblir den
skjult for de uten eksplisitt edge.
## Brukere er noder
En brukernode er en node med en kobling til Authentik for
autentisering. Alt annet — navn, preferanser, roller, relasjoner —
er noden og dens edges.
`users`-tabellen krymper til autentisering:
```sql
CREATE TABLE auth_identities (
node_id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE,
authentik_sub TEXT UNIQUE NOT NULL, -- Authentik subject ID
email TEXT UNIQUE NOT NULL
);
```
Brukerens profil, innstillinger og metadata lever på noden (JSONB)
eller som edges. Autentiseringstabellen er en tynn bro mellom
"denne HTTP-sesjonen" og "denne noden i grafen."
## Aliaser
En bruker kan opprette aliaser — separate noder som representerer
en offentlig persona eller anonym identitet.
```
Aleks (hidden) ──alias──→ Bjørn (readable)
```
Bjørn er en egen node med eget navn, egen profil, egen visibility.
Omverdenen ser bare Bjørn. `alias`-edgen er en **systemedge**
usynlig ved traversering, aldri eksponert utad. Ellers ville
"hvem kontrollerer Bjørn?" lekke Aleks' identitet.
### Én flate, kontekst bestemmer identitet
Aleks ser *alt* i sin mottaksflate — egne noder og alt som
kommer til Bjørn. Han svarer fra sin egen flate. Systemet
bestemmer `created_by` basert på konteksten:
- NN skrev til Bjørn → Aleks svarer → meldingen stemplet
med Bjørn som `created_by`. Automatisk, ingen bytte.
- Vegard skrev til Aleks direkte → svaret kommer fra Aleks.
**Ingen manuell identitetsbytte.** Aleks trenger aldri å "logge
inn som Bjørn" eller bytte modus. Konteksten (hvilken samtale,
hvilken kanal, hvem mottakeren kjenner) bestemmer hvilken node
som er avsender.
### Identitetsvalg ved tvetydighet
Noen ganger kjenner mottakeren *både* personen og aliaset.
Vegard kjenner Aleks og vet hvem Bjørn er. Hvis Vegard sender
en melding til Bjørn under en episode, og Aleks svarer:
- **Default:** svaret kommer fra Bjørn (konteksten er Bjørn).
- **Valg:** systemet vet at Vegard har edge til Aleks. Aleks
kan velge "svar som Aleks" — da startes en ny, separat
samtale mellom Vegard og Aleks.
**Alias-grensen brytes aldri automatisk.** Bare med eksplisitt
handling fra personen bak aliaset.
### Flere aliaser
Ingenting stopper en bruker fra å ha flere aliaser — Bjørn for
podcast, et anonymt alias for publisering, seg selv for privat.
Hver er en node med en `alias`-edge fra brukernoden. Alle samles
i én mottaksflate.
### Alias vs. owner
| Edge | Betydning | Eksempel |
|------|-----------|----------|
| `alias` | Jeg *er* denne noden. Usynlig systemedge. | Aleks → Bjørn |
| `owner` | Jeg *forvalter* denne noden. Synlig for medlemmer. | Vegard → Sidelinja |
`alias` = identitet. `owner` = ansvar. Aleks *er* Bjørn. Vegard
*eier* Sidelinja, men han *er* ikke Sidelinja.
## Team er noder
Et team er en node med `member`-edges til brukernoder. Gi teamet
tilgang til en samlings-node → alle teammedlemmer arver tilgang
via transitiv traversering. Ingen egen team-mekanisme.
```
Vegard (node) ──member──→ Podcastteamet (node) ──member──→ Sidelinja (node)
Trond (node) ──member──→ Podcastteamet (node)
```
Trond får tilgang til alt under Sidelinja fordi han er medlem av
Podcastteamet som er medlem av Sidelinja. Tilgangsmatrisen beregner
dette transitivt.
## Samlings-noder
En samlings-node er en vanlig node som fungerer som gravitasjonspunkt.
"Sidelinja" er en samlings-node Vegard oppretter og eier. Trond
kobles på — direkte eller via et team. Andre inviteres inn.
Det kan finnes mange samlings-noder — eller få. Et podcast-prosjekt,
en research-samling, en vennegjeng. De er ikke noe spesielt i
datamodellen — bare noder med edges til andre noder.
En innholdsnode kan ha edge til flere samlings-noder. En faktoid om
en gjest er relevant for både podcast-prosjektet og research-samlingen.
Ikke kopi — samme node, to edges.
### Felles kontekst
En samlings-node bærer kontekst (i JSONB) som arves av tilknyttede
noder:
- **Pruning-profil** — hvor aggressivt slettes binærdata?
- **Tema** — visuelt uttrykk
- **AI-konfigurasjon** — prompts, modell, regler
- **Default synlighet** — for nye noder opprettet i denne konteksten
- **Kapasitet** — ressursgrenser for maskinrommet
Konflikter (node med edge til to samlings-noder med ulike
pruning-profiler) løses med: mest konservativ vinner, eller
eier-edge bestemmer.
## Tilgangsroller
| Rolle | Hva den gir |
|-------|------------|
| `owner` | Full kontroll — slette, endre tilgang, endre innstillinger |
| `admin` | Kan invitere/fjerne andre, endre konfigurasjon |
| `member` | Kan gi input og motta |
| `reader` | Kan kun motta (observatør, lytter) |
Roller er en egenskap på edgen, ikke på noden. Vegard har `owner`-edge
til Sidelinja og `member`-edge til Research-gruppa. Samme node, ulike
roller i ulike kontekster.
## Tilgangsmatrise — spesifikasjon
### Skjema
```sql
CREATE TYPE access_level AS ENUM ('reader', 'member', 'admin', 'owner');
CREATE TABLE node_access (
subject_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
object_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
access access_level NOT NULL,
via_edge UUID REFERENCES edges(id) ON DELETE CASCADE,
PRIMARY KEY (subject_id, object_id)
);
CREATE INDEX idx_na_subject ON node_access (subject_id);
```
`subject_id` er noden som har tilgang (bruker eller team).
`object_id` er noden det gis tilgang til.
`via_edge` peker på edgen som ga tilgangen — for debugging og
deterministisk revokering.
### RLS-policy
```sql
CREATE POLICY node_read ON nodes FOR SELECT
USING (
created_by = current_node_id()
OR id IN (
SELECT object_id FROM node_access
WHERE subject_id = current_node_id()
)
OR visibility >= 'discoverable'
);
```
`current_node_id()` returnerer brukernodens id fra sesjonen.
Tre sjekker i prioritert rekkefølge:
1. Egne noder (`created_by`) — instant
2. Eksplisitt tilgang via matrisen — indeksert lookup
3. Offentlig synlige noder — kolonne-sjekk
Merk: `discoverable` gir kun at noden *finnes* i søkeresultater.
Innholdet filtreres i applikasjonslaget basert på visibility-nivå.
### Matrise-oppdatering
Matrisen oppdateres **når edges endres**, ikke ved lesing.
Én transaksjon: edge + matrise-oppdatering. Alltid synkront —
ingen vindu med stale tilgang.
**Transitiv tilgang:** Når Trond får `member`-edge til Sidelinja,
beregnes hans tilgang til alle noder med edge til Sidelinja.
```
Brukernode → samlings-node → innholdsnoder: 2 hopp
Brukernode → team → samlings-node → innholdsnoder: 3 hopp
Brukernode → team → samlings-node → komm.node → noder: 4 hopp
```
**Beregningsfunksjon:**
```sql
CREATE OR REPLACE FUNCTION recompute_access(
p_subject_id UUID, -- bruker- eller teamnode
p_root_node_id UUID, -- noden det gis tilgang til
p_access access_level,
p_via_edge UUID
) RETURNS void AS $$
BEGIN
-- Direkte tilgang til roten
INSERT INTO node_access (subject_id, object_id, access, via_edge)
VALUES (p_subject_id, p_root_node_id, p_access, p_via_edge)
ON CONFLICT (subject_id, object_id)
DO UPDATE SET access = GREATEST(node_access.access, p_access);
-- Transitiv: noder som tilhører roten
INSERT INTO node_access (subject_id, object_id, access, via_edge)
SELECT p_subject_id, e.source_id, p_access, p_via_edge
FROM edges e
WHERE e.target_id = p_root_node_id
AND e.edge_type = 'belongs_to'
ON CONFLICT (subject_id, object_id)
DO UPDATE SET access = GREATEST(node_access.access, p_access);
-- Hvis subject er et team: propager til alle teammedlemmer
INSERT INTO node_access (subject_id, object_id, access, via_edge)
SELECT e.source_id, na.object_id, na.access, na.via_edge
FROM node_access na
JOIN edges e ON e.target_id = p_subject_id
AND e.edge_type = 'member_of'
WHERE na.subject_id = p_subject_id
ON CONFLICT (subject_id, object_id)
DO UPDATE SET access = GREATEST(node_access.access, EXCLUDED.access);
-- Team-transitivitet: arv tilgang fra teamet.
-- Når en bruker melder seg inn i et team, arver brukeren
-- all tilgang teamet allerede har til andre noder.
INSERT INTO node_access (subject_id, object_id, access, via_edge)
SELECT p_subject_id, na.object_id, na.access, na.via_edge
FROM node_access na
WHERE na.subject_id = p_root_node_id
ON CONFLICT (subject_id, object_id)
DO UPDATE SET access = GREATEST(node_access.access, EXCLUDED.access);
END;
$$ LANGUAGE plpgsql;
```
Detaljer (hvilke edge-typer som gir transitiv tilgang, maks dybde)
avklares ved implementering.
### Matrisestørrelse
Sparse matrise. Typisk bruker med tilgang til 2-3 samlings-noder
med noen tusen noder hver: ~10k rader per bruker. Med 50 brukere:
~500k rader. Trivielt for PG.
## Brukeropplevelse
Når du logger inn ser du:
- **Dine aktive samtaler** — kommunikasjonsnoder med edge til deg
- **Dine noder** — alt du har skapt eller er koblet til
- **Dine samlings-noder** — grupperer kontekst, filtrering er frivillig
- **Din mottaksflate** — det som er relevant for deg nå
Å filtrere etter samlings-node ("vis bare podcast-prosjektet") er et
filter, ikke en modebytte.
## Forhold til andre retninger
- [Rom, ikke forum](rom_ikke_forum.md) — "rommet" er summen av dine
edges, ikke en container du går inn i
- [Universell input og mottak](universell_input.md) — mottaksflaten
er "noder med edge til *meg*", filtrert og vektet
- [Maskinrommet](maskinrommet.md) — eier matrise-oppdatering og leser
samlings-node-edges for kontekst (pruning, kapasitet, AI-konfig)
- [Datalaget](datalaget.md) — matrisen lever i PG, indeksert for
RLS-ytelse