From 858499e8ecbc8a03d09b028819bef1f8c4bee7ec Mon Sep 17 00:00:00 2001 From: Sebastian Hedtrich Date: Mon, 17 Aug 2026 23:27:08 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20personenbezogene=20Daten=20aus=20Compani?= =?UTF-8?q?on-Klartextkanal=20ausschlie=C3=9Fen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PlainEventStore.Allowed: Grade/ExamResult entfernt (beide tragen StudentId + Notenwert/-kommentar, PlainSyncEvent.Payload läuft aber als Klartext-JSON über den Server, anders als der AES-256-GCM- verschlüsselte Desktop-Kanal). Nur noch WorkTask/Lesson erlaubt. - Regressionstest PlainEventStoreTests ergänzt. - TODO.md 10.3.3 abgehakt, inkl. deutlichem Warnhinweis: die geplante Mitarbeitsnoten-Erfassung per Companion-App ist damit bewusst blockiert, bis 10.3.1 (Schlüsselaustausch) + eine echte Payload- Verschlüsselung für PlainSyncEvent existieren. Co-Authored-By: Claude Sonnet 5 --- LehrerApp.Api.Tests/PlainEventStoreTests.cs | 54 +++++++++++++++++++++ LehrerApp.Api/PlainEventStore.cs | 6 ++- TODO.md | 21 +++++++- 3 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 LehrerApp.Api.Tests/PlainEventStoreTests.cs diff --git a/LehrerApp.Api.Tests/PlainEventStoreTests.cs b/LehrerApp.Api.Tests/PlainEventStoreTests.cs new file mode 100644 index 0000000..7dc6720 --- /dev/null +++ b/LehrerApp.Api.Tests/PlainEventStoreTests.cs @@ -0,0 +1,54 @@ +using LehrerApp.Sync.Models; +using Xunit; + +namespace LehrerApp.Api.Tests; + +public sealed class PlainEventStoreTests +{ + [Theory] + [InlineData("Grade")] + [InlineData("ExamResult")] + public void Push_PersonenbezogenerEntityTyp_WirdAbgelehnt(string entityType) + { + using var temp = new TempEventStore(); + var evt = new PlainSyncEvent { EntityType = entityType, EntityId = "1", Operation = "Save" }; + + var result = temp.Plain.Push("user", [evt]); + + Assert.Contains(evt.EventId, result.RejectedEventIds); + } + + [Theory] + [InlineData("WorkTask")] + [InlineData("Lesson")] + public void Push_UnkritischerEntityTyp_WirdAngenommen(string entityType) + { + using var temp = new TempEventStore(); + var evt = new PlainSyncEvent { EntityType = entityType, EntityId = "1", Operation = "Save" }; + + var result = temp.Plain.Push("user", [evt]); + + Assert.DoesNotContain(evt.EventId, result.RejectedEventIds); + } + + private sealed class TempEventStore : IDisposable + { + private readonly string _directory = Path.Combine( + Path.GetTempPath(), $"lehrerapp-api-tests-{Guid.NewGuid():N}"); + private readonly EventStore _events; + public PlainEventStore Plain { get; } + + public TempEventStore() + { + Directory.CreateDirectory(_directory); + _events = new EventStore(_directory); + Plain = new PlainEventStore(_events); + } + + public void Dispose() + { + _events.Dispose(); + if (Directory.Exists(_directory)) Directory.Delete(_directory, recursive: true); + } + } +} diff --git a/LehrerApp.Api/PlainEventStore.cs b/LehrerApp.Api/PlainEventStore.cs index 12f035b..5e8775c 100644 --- a/LehrerApp.Api/PlainEventStore.cs +++ b/LehrerApp.Api/PlainEventStore.cs @@ -4,8 +4,10 @@ namespace LehrerApp.Api; public class PlainEventStore(EventStore eventStore) { - private static readonly HashSet Allowed = - ["Grade", "ExamResult", "WorkTask", "Lesson"]; + // Grade/ExamResult bewusst NICHT erlaubt: PlainSyncEvent.Payload ist Klartext-JSON (siehe + // SyncModels.cs), beide Typen tragen aber eine StudentId plus Notenwert/-kommentar - + // personenbezogene Daten dürfen laut TODO.md 10.3.3 nicht unverschlüsselt übertragen werden. + private static readonly HashSet Allowed = ["WorkTask", "Lesson"]; public PlainPushResponse Push(string userId, List events) { diff --git a/TODO.md b/TODO.md index 90823ae..fb741e1 100644 --- a/TODO.md +++ b/TODO.md @@ -1411,8 +1411,25 @@ die Docker-Verifikation unter 10.2.4 (kein Docker im Entwicklungsstand verfügba ### 10.3 Verschlüsselung - [ ] **10.3.1** Schlüsselübertragung auf ein zweites Gerät (QR-Code oder Passphrase). - [ ] **10.3.2** Warnung und Wiederherstellungspfad bei verlorenem Schlüssel. -- [ ] **10.3.3** Prüfen, welche Daten unverschlüsselt über `PlainEventStore` laufen — - personenbezogene Daten dürfen das nicht. +- [x] **10.3.3** Prüfen, welche Daten unverschlüsselt über `PlainEventStore` laufen — + personenbezogene Daten dürfen das nicht. `Grade`/`ExamResult` (beide mit `StudentId` plus + Notenwert/-kommentar) trugen bisher personenbezogene Daten im Klartext über den + Companion/WebApp-Kanal (`PlainSyncEvent.Payload` ist unverschlüsseltes JSON, anders als der + AES-256-GCM-verschlüsselte Desktop-Kanal — der Server sieht den Klartext also nicht nur + während der Übertragung, sondern speichert ihn auch dauerhaft unverschlüsselt in der + EventStore-Collection). Aus `PlainEventStore.Allowed` entfernt — nur noch `WorkTask`/`Lesson` + (reine Lehrer-Planungsdaten ohne `StudentId`) laufen über diesen Kanal. + + > ⚠️ **BEWUSSTE BLOCKADE, gehört zu geplanter Companion-App-Funktion:** Mitarbeitsnoten + > sollen laut Plan später über eine Companion-App erfassbar sein — das ist genau `Grade` mit + > `Category = Participation`. **Das geht mit dem aktuellen Stand nicht**, und zwar absichtlich: + > `PlainEventStore.Push` lehnt `Grade`/`ExamResult`-Events NICHT mit einem Fehler/4xx ab, + > sondern **schluckt sie still** — `Success = true`, das Event landet nur in + > `RejectedEventIds` statt gespeichert zu werden. Wer das nicht kennt, sucht stundenlang, + > warum Mitarbeitsnoten aus der Companion-App im Server nie ankommen. Bevor eine + > Companion-App Noten schreiben soll, muss zuerst 10.3.1 (Schlüsselaustausch) + eine echte + > Payload-Verschlüsselung für `PlainSyncEvent` gebaut werden — erst danach `Grade` wieder in + > `PlainEventStore.Allowed` aufnehmen. - [ ] **10.3.4** Bekannte v1-Einschränkung aus 10.1.7: weiche Geschäftsregeln greifen beim Anwenden eingehender Sync-Ereignisse nicht, nur harte LiteDB-Unique-Constraints. Bei mehreren eigenen Geräten in Randfällen möglich, dass sich Datenstände leicht unterscheiden. Für v1 bewusst