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>
337 lines
12 KiB
Markdown
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
|