|
1 | 1 | Notwendige Fixes und ToDos für den Kanban Editor |
2 | 2 | =============================================== |
3 | 3 |
|
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