diff --git a/docs/03-auth-und-rechte.md b/docs/03-auth-und-rechte.md index 76acc8d..d9da54d 100644 --- a/docs/03-auth-und-rechte.md +++ b/docs/03-auth-und-rechte.md @@ -99,10 +99,21 @@ Für Abschnitte und Verlaufseinträge gilt dasselbe eine Ebene tiefer: `minClear nicht über die eigene Stufe heben, und eine Erhöhung wirkt rückwirkend auf alle Versionen (siehe `docs/05-akten-und-vorlagen.md`). -**Ausnahmsweiser Zugang** läuft über `CaseParticipant`: wer als Beteiligter eingetragen ist, -liest die Akte unabhängig von seiner Stufe. Das ist der vorgesehene Weg, jemanden gezielt -hinzuzuziehen, ohne ihm allgemein eine höhere Stufe zu geben. Beteiligte hinzuzufügen setzt -`case.edit` voraus und wird protokolliert. +**Ausnahmsweiser Zugang** läuft über `CaseParticipant`, aber **nur für Arbeitsrollen**. Die +Beteiligtenliste beantwortet zwei ganz verschiedene Fragen: + +| Rolle | Bedeutung | Öffnet die Akte | +|---|---|---| +| `LEAD`, `INVESTIGATOR`, `OBSERVER` | bearbeitet den Fall | ja, unabhängig von der Stufe | +| `WITNESS`, `SUBJECT`, `SUSPECT`, `ACCOMPLICE` | ist Gegenstand des Falls | nein | + +Die erste Gruppe ist der vorgesehene Weg, jemanden gezielt hinzuzuziehen, ohne ihm allgemein +eine höhere Stufe zu geben. Die zweite ist eine Angabe über den Fall und keine Berechtigung an +ihm — sonst läse ein Betroffener oder Beschuldigter mit Discord-Konto die Ermittlung gegen sich +selbst mit. Die Liste steht in `lib/cases/terms.ts` als `WORKING_ROLES`; wer sie erweitert, +entscheidet damit über Zugriff. + +Beteiligte hinzuzufügen setzt `case.edit` voraus und wird protokolliert. Die Gegenprobe bleibt der `CaseExclusion`: er schlägt jede Stufe und auch den Admin. diff --git a/docs/13-entscheidungen.md b/docs/13-entscheidungen.md index db6222a..159426b 100644 --- a/docs/13-entscheidungen.md +++ b/docs/13-entscheidungen.md @@ -1107,3 +1107,41 @@ Fix grün und wurde wieder entfernt — eine Prüfung, die nicht anschlägt, ist keine. `web/tests/thema-vorab.test.ts` prüft stattdessen die drei Dinge am Skript, die zusammenpassen müssen: Nonce, Hydrierungsausnahme und derselbe Speicherschlüssel wie im Umschalter. + +## Beteiligte, Suche, Abteilungen + +### Nur Arbeitsrollen öffnen die Akte + +Anlass war ein einfacher Wunsch: neben Zeuge und Betroffenem sollte man auch **Täter** und +**Mittäter** eintragen können. Dabei fiel eine Lücke auf, die schon vorher bestand. + +Ein Beteiligter durfte die Akte lesen, gleich in welcher Rolle — so stand es in docs/03 und so +war es umgesetzt. Bei „Betroffener" fiel das nicht auf. Mit „Täter" wäre es offensichtlich +falsch geworden: der Beschuldigte hätte die Ermittlung gegen sich selbst mitgelesen, sofern er +ein Discord-Konto hat. + +Die Beteiligtenliste beantwortet eben zwei Fragen — *wer bearbeitet den Fall* und *um wen geht +es darin* —, und nur die erste ist eine Berechtigung. `WORKING_ROLES` in `lib/cases/terms.ts` +nennt die drei, die die Akte öffnen: Ermittlungsführung, Ermittler, Beobachter. Zeuge, +Betroffener, Täter und Mittäter öffnen nichts. + +Das gilt an allen vier Stellen, die auf Beteiligung geprüft haben: Aktenzugriff, +Dokumentzugriff über die besitzende Akte, „meine Fälle" auf der Übersicht und der Filter „nur +meine" in der Suche. Ein Test geht alle vier Nicht-Arbeitsrollen einzeln durch. + +### Die Suche zeigt, wo sonst noch etwas liegt + +Wer nach einem Namen suchte, landete auf den Fallakten und erfuhr nie, dass es zu demselben +Namen auch eine Personalakte gibt — die Suche fand sie, aber hinter einem Reiter, auf den +nichts hindeutete. Die Reiter tragen jetzt ihre Trefferzahl, und ein leeres Ergebnis nennt +ausdrücklich, wo etwas liegt. + +Dabei fiel auf, dass `searchPersonnel` das Recht `personnel.read` **nicht** prüfte. Die +Kommandopalette tat es, die Aktenseite tat es, die Suche nicht: über `/suche?kind=personnel` +kam jeder mit ausreichender Stufe an eine Liste aller Personalakten mit Namen, Dienstgrad und +Etiketten. Die Prüfung steht jetzt in `lib/search/service.ts` und damit an der Stelle, die +diese Akten herausgibt, nicht in der Seite. + +Die Trefferzahl einer Art, die es für den Nutzer nicht gibt, ist `null` und nicht `0`: eine +Null wäre die Auskunft „hier ist nichts", und die steht ihm nicht zu. Der Reiter fehlt dann +ganz. diff --git a/packages/db/prisma/migrations/20260904090000_taeter_und_mittaeter/migration.sql b/packages/db/prisma/migrations/20260904090000_taeter_und_mittaeter/migration.sql new file mode 100644 index 0000000..902c26b --- /dev/null +++ b/packages/db/prisma/migrations/20260904090000_taeter_und_mittaeter/migration.sql @@ -0,0 +1,13 @@ +-- Täter und Mittäter als Beteiligtenrollen. +-- +-- Damit wird eine Lücke sichtbar, die es schon vorher gab: ein Beteiligter darf +-- die Akte lesen, gleich in welcher Rolle. Bei „Betroffener" fiel das nicht auf, +-- bei „Täter" wäre es offensichtlich falsch — der Beschuldigte läse die +-- Ermittlung gegen sich selbst mit. +-- +-- Die Prüfung in lib/access/caseFilter.ts unterscheidet deshalb ab jetzt +-- Arbeitsrollen (LEAD, INVESTIGATOR, OBSERVER) von Angaben über den Fall +-- (WITNESS, SUBJECT, SUSPECT, ACCOMPLICE). Nur die ersten öffnen die Akte. + +ALTER TYPE "ParticipantRole" ADD VALUE IF NOT EXISTS 'SUSPECT'; +ALTER TYPE "ParticipantRole" ADD VALUE IF NOT EXISTS 'ACCOMPLICE'; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index b8b5e02..f8e8285 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -410,12 +410,21 @@ model CaseParticipant { @@index([personnelFileId]) } +/// Rollen eines Beteiligten. +/// +/// Zwei Gruppen, und der Unterschied entscheidet ueber den Zugriff: LEAD, +/// INVESTIGATOR und OBSERVER **arbeiten** an der Akte und duerfen sie deshalb +/// lesen. WITNESS, SUBJECT, SUSPECT und ACCOMPLICE sind Angaben **ueber** den +/// Fall und verschaffen keinen Zugang — sonst laese der Beschuldigte die +/// Ermittlung gegen sich selbst mit. Siehe lib/cases/terms.ts. enum ParticipantRole { LEAD INVESTIGATOR + OBSERVER WITNESS SUBJECT - OBSERVER + SUSPECT + ACCOMPLICE } /// Schlaegt jede Freigabestufe. Deckt den Fall ab, dass gegen einen hochrangigen diff --git a/web/app/(app)/suche/page.tsx b/web/app/(app)/suche/page.tsx index 2798545..34899e2 100644 --- a/web/app/(app)/suche/page.tsx +++ b/web/app/(app)/suche/page.tsx @@ -3,7 +3,12 @@ import Link from 'next/link' import { db } from '@/lib/db' import { requireUser } from '@/lib/auth/current' import { parseFilters, toSearchParams, SEARCH_KINDS } from '@/lib/search/query' -import { searchCases, searchDocuments, searchPersonnel } from '@/lib/search/service' +import { + searchCases, + searchCounts, + searchDocuments, + searchPersonnel, +} from '@/lib/search/service' import { builtinViews } from '@/lib/search/views' import { de } from '@/lib/i18n/de' import { StampMark, classificationEdgeStyle } from '@/components/StampMark' @@ -44,12 +49,17 @@ export default async function SearchPage({ ]) const levelName = new Map(levels.map((l) => [l.level, l.name])) - const ergebnis = + const [ergebnis, zahlen] = await Promise.all([ filters.kind === 'document' - ? await searchDocuments(user.permissions, filters) + ? searchDocuments(user.permissions, filters) : filters.kind === 'personnel' - ? await searchPersonnel(user.permissions, filters) - : await searchCases(user.permissions, filters) + ? searchPersonnel(user.permissions, filters) + : searchCases(user.permissions, filters), + // Die Zahlen der anderen Arten. Ohne sie sucht man nach einem Namen, landet + // auf den Fallakten und erfährt nie, dass es dazu auch eine Personalakte + // gibt — der Reiter sieht leer aus wie jeder andere. + searchCounts(user.permissions, filters), + ]) const statusTags = filters.kind === 'personnel' ? await listStatusTags() : [] @@ -64,16 +74,26 @@ export default async function SearchPage({ -