Skip to content

Commit cec26f4

Browse files
authored
Merge pull request #121 from edufeed-org/security-fix
Sicherheits-Guards für Column-Patch- und Card-Events
2 parents ef90f07 + 6fda63b commit cec26f4

File tree

5 files changed

+707
-33
lines changed

5 files changed

+707
-33
lines changed

docs/TO-FIX/TODO.md

Lines changed: 158 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,161 @@
11
Notwendige Fixes und ToDos für den Kanban Editor
22
===============================================
33

4-
## Neuer Task: Sicherheits-Guard fuer Column-Patch-Events (Kind 8571)
5-
6-
- Problem: Column-Patch-Events (Spaltenname/-farbe/-reihenfolge) werden aktuell ohne harten Rollencheck angewendet.
7-
- Risiko: Fremde Publisher koennen potenziell Spalten-Metadaten beeinflussen, wenn Event-Filter greifen.
8-
- Ziel: Inbound-Patch nur akzeptieren, wenn `event.pubkey` entweder `board.author` oder in `board.maintainers` enthalten ist.
9-
- Umsetzung:
10-
- Guard im Patch-Handler/Apply-Pfad einbauen (`publisherPubkey` verbindlich validieren).
11-
- Ungueltige Events mit Debug/Warn skippen (kein Apply, kein local persist).
12-
- Vorhandene Owner-only-Regel fuer 30301 unveraendert lassen.
13-
- Tests:
14-
- `editor`-Patch wird angewendet.
15-
- `owner`-Patch wird angewendet.
16-
- Fremder Pubkey wird verworfen.
17-
- LWW-Verhalten bleibt intakt.
18-
19-
## Neuer Task: Sicherheits-Guard fuer Card-Events (Kind 30302)
20-
21-
- Problem: Card-Events werden aktuell ohne harten Publisher-Rollencheck verarbeitet.
22-
- Risiko: Fremde Publisher koennen Card-Inhalte, Positionen (rank/column) oder Loeschungen im Client beeinflussen.
23-
- Ziel: Inbound-Card nur akzeptieren, wenn `event.pubkey` entweder `board.author` oder in `board.maintainers` enthalten ist.
24-
- Optional: `card.author` nicht als harte Sperre fuer Updates erzwingen, damit Maintainer kollaborativ alle Cards bearbeiten koennen.
25-
- Umsetzung:
26-
- Guard in `handleCardEvent` vor `upsertCardFromNostr()` / `upsertCardToBackgroundBoard()` einbauen.
27-
- Ungueltige Events mit Debug/Warn skippen (kein Apply, kein local persist).
28-
- Bestehende LWW-/Tie-Break-Logik unveraendert lassen.
29-
- Tests:
30-
- Owner-Card-Update wird angewendet.
31-
- Maintainer-Card-Update wird angewendet.
32-
- Fremder Pubkey wird verworfen.
33-
- Background-Board-Sync respektiert den Guard.
4+
## ~~Neuer Task: Sicherheits-Guard fuer Column-Patch-Events (Kind 8571)~~ ✅ DONE (2025-02-20)
5+
6+
- ✅ Guard in `columnOrderPatch.ts` nach Board-Zugehoerigkeits-Check eingefuegt
7+
- ✅ Nutzt `currentBoard.isMaintainer(event.pubkey)` — Owner + Maintainers akzeptiert, Fremde rejected
8+
- ✅ Guard nur aktiv wenn `currentBoard.author` gesetzt (kein Breaking Change fuer lokale Boards)
9+
- ✅ Tests: `columnOrderPatch.authorization.spec.ts` (11 Tests, alle gruen)
10+
11+
## ~~Neuer Task: Sicherheits-Guard fuer Card-Events (Kind 30302)~~ ✅ DONE (2025-02-20)
12+
13+
- ✅ Guard in `card.ts` vor `upsertCardFromNostr()` / `upsertCardToBackgroundBoard()` eingefuegt
14+
- ✅ Current-Board + Background-Board Guards (Background via `BoardStorage.loadBoard()`)
15+
- ✅ Nutzt `board.isMaintainer(event.pubkey)` — Maintainer koennen kollaborativ alle Cards bearbeiten
16+
- ✅ Guard nur aktiv wenn `board.author` gesetzt; Background-Board ohne lokale Kopie → Guard uebersprungen
17+
- ✅ Tests: `card.authorization.spec.ts` (12 Tests, alle gruen)
18+
19+
20+
## Lösungsvorschlag: Sicherheits-Guards für Kind 8571 & Kind 30302
21+
22+
Beide Event-Handler (Column-Patch und Card) nehmen aktuell Events von **jedem** Publisher an. Das Ziel: Inbound-Events nur akzeptieren, wenn `event.pubkey` entweder `board.author` oder in `board.maintainers` enthalten ist. Die bestehende Methode `Board.isMaintainer(pubkey)` (BoardModel.ts) deckt beides ab und wird als zentrale Prüfung genutzt.
23+
24+
**Steps**
25+
26+
### 1. Guard in Column-Patch-Handler einfügen
27+
28+
**Datei:** columnOrderPatch.ts
29+
30+
- **Wo:** Nach dem Board-Zugehörigkeits-Check (~Zeile 54, nach `if (!aOk && !dOk)`) und **vor** dem `boardStore.applyColumnOrderPatchFromNostr()`-Aufruf (~Zeile 105).
31+
- **Was:** Neuer Block, der `currentBoard.isMaintainer(event.pubkey)` prüft. Wenn `currentBoard.author` gesetzt ist (= Nostr-Board) und der Pubkey kein Maintainer ist → `return false` mit Debug-Log.
32+
- **Edge-Case:** Wenn `currentBoard.author` leer/undefined ist (lokales Board ohne Nostr), wird der Guard übersprungen — kein Breaking Change für Offline-Boards.
33+
34+
### 2. Guard in Card-Event-Handler einfügen
35+
36+
**Datei:** card.ts
37+
38+
- **Wo:** Im `try`-Block, **nach** der Ermittlung von `targetBoardId` (~Zeile 79) und **vor** den `upsertCardFromNostr()` / `upsertCardToBackgroundBoard()`-Aufrufen (~Zeile 128-133).
39+
- **Was:** Zwei Pfade je nach Ziel-Board:
40+
- **Current Board** (`targetBoardId === currentBoard.id`): Prüfe `currentBoard.isMaintainer(cardEvent.pubkey)`.
41+
- **Background Board** (`targetBoardId !== currentBoard.id`): Lade Board via `BoardStorage.loadBoard(targetBoardId)` und prüfe `loadedBoard.isMaintainer(cardEvent.pubkey)`. Wenn Board nicht ladbar → Guard überspringen (Board existiert lokal nicht, kein Referenz-Author bekannt).
42+
- **Edge-Case:** Wie bei 8571 — Guard nur aktiv wenn `board.author` gesetzt ist.
43+
44+
### 3. Tests für Column-Patch-Guard
45+
46+
**Neue Datei:** `src/lib/stores/boardstore/nostr/handlers/columnOrderPatch.authorization.spec.ts`
47+
48+
Test-Pattern analog zu board.lww-shared.spec.ts: Board-Mock mit `author` + `maintainers`, fake Events mit verschiedenen Pubkeys.
49+
50+
**Test-Cases:**
51+
1. **Owner-Patch wird angewendet**`event.pubkey === board.author``applyColumnOrderPatchFromNostr` wird aufgerufen.
52+
2. **Maintainer-Patch wird angewendet**`event.pubkey` in `board.maintainers` → wird aufgerufen.
53+
3. **Fremder Pubkey wird verworfen**`event.pubkey` ist weder Owner noch Maintainer → `return false`, Store-Methode nicht aufgerufen.
54+
4. **Lokales Board (kein author)**`board.author` ist `undefined` → Guard wird übersprungen, Patch wird angewendet.
55+
5. **LWW-Verhalten intakt** — bestehende Tests in kanbanStore.columnPatch.spec.ts bleiben grün.
56+
57+
### 4. Tests für Card-Guard
58+
59+
**Neue Datei:** `src/lib/stores/boardstore/nostr/handlers/card.authorization.spec.ts`
60+
61+
Gleicher Mock-Ansatz wie card.lww-ms.spec.ts: `createBoardMock` erweitern um `author`, `maintainers` und `isMaintainer()`.
62+
63+
**Test-Cases:**
64+
1. **Owner-Card-Update wird angewendet**`event.pubkey === board.author``upsertCardFromNostr` aufgerufen.
65+
2. **Maintainer-Card-Update wird angewendet**`event.pubkey` in `board.maintainers` → aufgerufen.
66+
3. **Fremder Pubkey wird verworfen** — weder Owner noch Maintainer → kein Aufruf, kein Error.
67+
4. **Background-Board-Sync respektiert Guard** — Background-Board mit `author` geladen, fremder Pubkey → `upsertCardToBackgroundBoard` nicht aufgerufen.
68+
5. **Lokales Board (kein author)** — Guard übersprungen, Card wird normal verarbeitet.
69+
70+
### 5. TODO.md aktualisieren
71+
72+
Beide Tasks in TODO.md nach Implementierung als erledigt markieren.
73+
74+
**Verification**
75+
- `pnpm run test:unit` — alle bestehenden Tests + neue Guard-Tests grün
76+
- `pnpm run check` — TypeScript-Checks bestehen
77+
- Manuelle Verifikation: In der Browser-Konsole prüfen, dass bei fremden Events ein `[ColumnOrderPatch] rejected` bzw. `[CardEvent] rejected` Debug-Log erscheint
78+
79+
**Decisions**
80+
- **Guard-Platzierung im Handler (nicht im Store):** Konsistent mit dem bestehenden Board-Guard in board.ts. Der Handler ist die richtige Schicht für Autorisierung — der Store bleibt "dumb".
81+
- **`isMaintainer()` statt manueller Prüfung:** Die Methode existiert bereits und deckt Owner + Maintainers ab. Kein neuer Code im Model nötig.
82+
- **Guard nur aktiv wenn `board.author` gesetzt:** Verhindert Breaking Changes für rein lokale Boards ohne Nostr-Anbindung.
83+
- **Background-Board via `BoardStorage.loadBoard()`:** Für Card-Events, die nicht für das aktive Board bestimmt sind, wird das Ziel-Board aus localStorage geladen. Wenn nicht vorhanden → Guard übersprungen (kein Referenz-Author bekannt, kann nicht validieren).
84+
85+
86+
---
87+
88+
## PR Description
89+
90+
### Authorization Guards for Inbound Nostr Events (Kind 8571 & 30302)
91+
92+
**Problem**
93+
94+
Column-Patch-Events (Kind 8571) und Card-Events (Kind 30302) wurden bisher ohne Publisher-Rollencheck verarbeitet. Jeder Nostr-Nutzer, dessen Events die Relay-Filter passieren, konnte potenziell Spalten-Metadaten (Name, Farbe, Reihenfolge) und Card-Inhalte (Titel, Beschreibung, Position, Löschung) im lokalen Client beeinflussen.
95+
96+
Der Board-Event-Handler (Kind 30301) hatte bereits einen Owner-only Guard — dieser PR schliesst die Lücke für die beiden anderen Event-Typen.
97+
98+
**Solution**
99+
100+
Autorisierungs-Guards in beiden Event-Handlern, konsistent mit dem bestehenden Pattern in `board.ts`:
101+
102+
- **Column-Patch (Kind 8571):** Guard in `columnOrderPatch.ts` nach Board-Zugehoerigkeits-Check. Nutzt `currentBoard.isMaintainer(event.pubkey)` — akzeptiert Owner + Maintainers, rejected alle anderen mit Debug-Log.
103+
- **Card (Kind 30302):** Guard in `card.ts` vor `upsertCardFromNostr()` / `upsertCardToBackgroundBoard()`. Zwei Pfade:
104+
- Current Board: prüft `currentBoard.isMaintainer(eventPubkey)`
105+
- Background Board: lädt Board via `BoardStorage.loadBoard()`, prüft `backgroundBoard.isMaintainer(eventPubkey)`. Board lokal nicht vorhanden → Guard übersprungen.
106+
107+
Kein neuer Code im Model — die bestehende `Board.isMaintainer(pubkey)` Methode deckt Owner + Maintainers ab.
108+
109+
**Design Decisions**
110+
111+
- Guards nur aktiv wenn `board.author` gesetzt ist → kein Breaking Change für rein lokale Offline-Boards
112+
- Guard-Platzierung im Handler (nicht im Store) — konsistent mit `board.ts`, Handler ist die richtige Autorisierungs-Schicht
113+
- `card.author` wird bewusst **nicht** als harte Sperre erzwungen → Maintainer können kollaborativ alle Cards bearbeiten
114+
- Ungültige Events werden silent mit `console.debug` geloggt (kein Error, kein Apply, kein local persist)
115+
116+
**Changed Files**
117+
118+
| Datei | Änderung |
119+
|-------|----------|
120+
| `src/lib/stores/boardstore/nostr/handlers/columnOrderPatch.ts` | Authorization Guard eingefügt (Owner + Maintainers) |
121+
| `src/lib/stores/boardstore/nostr/handlers/card.ts` | Authorization Guard für Current- und Background-Board eingefügt |
122+
| `src/lib/stores/boardstore/nostr/handlers/columnOrderPatch.authorization.spec.ts` | **Neu** — 11 Tests |
123+
| `src/lib/stores/boardstore/nostr/handlers/card.authorization.spec.ts` | **Neu** — 12 Tests |
124+
| `docs/TO-FIX/TODO.md` | Tasks als erledigt markiert |
125+
126+
**Test Coverage (23 neue Tests)**
127+
128+
Column-Patch Guard (11 Tests):
129+
- Owner-Patch akzeptiert
130+
- Maintainer-Patch akzeptiert
131+
- Fremder Pubkey verworfen
132+
- Lokales Board ohne author → Guard übersprungen
133+
- Leerer author-String → Guard übersprungen
134+
- Fehlender event.pubkey → Guard übersprungen
135+
- Zweiter Maintainer akzeptiert
136+
- Deduplication intakt neben Guard
137+
- Board-Mismatch greift weiterhin vor Guard
138+
- Fallback wenn `isMaintainer` keine Funktion (plain object)
139+
- Stranger rejected mit Fallback
140+
141+
Card Guard (12 Tests):
142+
- Owner-Card-Update akzeptiert
143+
- Maintainer-Card-Update akzeptiert
144+
- Fremder Pubkey verworfen
145+
- Lokales Board ohne author → Guard übersprungen
146+
- Fehlender event.pubkey → Guard übersprungen
147+
- Zweiter Maintainer akzeptiert
148+
- Background-Board: Owner akzeptiert
149+
- Background-Board: Maintainer akzeptiert
150+
- Background-Board: Stranger rejected
151+
- Background-Board ohne author → Guard übersprungen
152+
- Background-Board lokal nicht vorhanden → Guard übersprungen, Event trotzdem gespeichert
153+
- LWW-Skip funktioniert weiterhin neben Guard
154+
155+
**Verification**
156+
157+
```bash
158+
pnpm run test:unit # 38/38 Handler-Tests grün (inkl. 23 neue)
159+
```
160+
161+
Keine Regressionen in bestehenden Tests (`board.lww-shared.spec.ts`, `card.lww-ms.spec.ts`, `deletion.spec.ts`).

0 commit comments

Comments
 (0)