Files
Harlekin e0b03ea336
Prüfung / Lint, Typen, Tests (push) Canceled after 0s
Prüfung / Images bauen (pdf) (push) Canceled after 0s
Prüfung / Images bauen (web) (push) Canceled after 0s
Prüfung / Images bauen (worker) (push) Canceled after 0s
Images / bauen (pdf, docker/pdf.Dockerfile) (push) Canceled after 0s
Images / bauen (web, docker/web.Dockerfile) (push) Canceled after 0s
Images / bauen (worker, docker/worker.Dockerfile) (push) Canceled after 0s
Gedankenstriche durch Bindestriche ersetzt
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die
README; an keiner Stelle Bedeutung, nur Zeichensetzung.
2026-09-04 08:54:52 +02:00

239 lines
7.8 KiB
TypeScript

// @vitest-environment node
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { createPrismaClient, loadEnvFromRepoRoot, type PrismaClient } from '@afis/db'
import { visibleCaseWhere, visibleEntryWhere } from '@/lib/access/caseFilter'
import type { EffectivePermissions, Permission } from '@/lib/auth/permissions'
/**
* Die Negativtests aus docs/03-auth-und-rechte.md.
*
* Sie laufen gegen eine echte Datenbank, weil genau das der Punkt ist: die
* Prüfung muss in der Abfrage stecken, nicht in einem Filter danach. Ein Test
* gegen eine ausgedachte Liste würde die Aussage nicht treffen.
*/
loadEnvFromRepoRoot()
const url = process.env.DIRECT_DATABASE_URL ?? process.env.DATABASE_URL
const suite = url ? describe : describe.skip
let db: PrismaClient
const marke = `zugriff-${Date.now()}`
function perms(
userId: string,
clearanceLevel: number,
options: { isAdmin?: boolean; permissions?: Permission[] } = {},
): EffectivePermissions {
return {
userId,
clearanceLevel,
permissions: new Set<Permission>(options.permissions ?? []),
isAdmin: options.isAdmin ?? false,
hasMembership: true,
}
}
suite('Sichtbarkeit von Akten', () => {
const ids = {
offen: `${marke}-offen`,
geheim: `${marke}-geheim`,
versiegelt: `${marke}-versiegelt`,
}
const nutzer = {
stufe1: `${marke}-stufe1`,
stufe2: `${marke}-stufe2`,
stufe5: `${marke}-stufe5`,
beteiligt: `${marke}-beteiligt`,
admin: `${marke}-admin`,
}
beforeAll(async () => {
db = createPrismaClient(url)
const template = await db.documentTemplate.create({
data: { key: `${marke}-vorlage`, version: 1, name: 'Prüfvorlage', fields: [] },
})
for (const id of Object.values(nutzer)) {
await db.user.create({ data: { id, username: id, displayName: id } })
}
const gemeinsam = { templateId: template.id, templateKey: template.key, createdById: nutzer.stufe5 }
await db.case.create({
data: {
...gemeinsam,
id: ids.offen,
caseNumber: `AfiS-9001-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,
title: 'Offener Vorgang',
classification: 1,
status: 'OPEN',
},
})
await db.case.create({
data: {
...gemeinsam,
id: ids.geheim,
caseNumber: `AfiS-9002-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,
title: 'Geheimer Vorgang',
classification: 4,
status: 'OPEN',
},
})
await db.case.create({
data: {
...gemeinsam,
id: ids.versiegelt,
caseNumber: `AfiS-9003-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,
title: 'Versiegelter Vorgang',
classification: 1,
status: 'SEALED',
},
})
// Als Ermittler an der geheimen Akte, obwohl seine Stufe nicht reicht.
// Die Rolle ist hier wesentlich: nur wer an der Akte arbeitet, öffnet sie.
await db.caseParticipant.create({
data: { caseId: ids.geheim, userId: nutzer.beteiligt, role: 'INVESTIGATOR' },
})
// Ausgeschlossen von der offenen Akte, obwohl Stufe 5 und Admin.
await db.caseExclusion.createMany({
data: [
{ caseId: ids.offen, userId: nutzer.stufe5, reason: 'Betroffener' },
{ caseId: ids.offen, userId: nutzer.admin, reason: 'Betroffener' },
],
})
})
afterAll(async () => {
await db.$disconnect()
})
async function sichtbar(p: EffectivePermissions): Promise<string[]> {
const rows = await db.case.findMany({
where: { AND: [visibleCaseWhere(p), { id: { in: Object.values(ids) } }] },
select: { id: true },
})
return rows.map((r) => r.id).sort()
}
it('zeigt einer Stufe 2 die Stufe-4-Akte nicht, auch nicht als Treffer', async () => {
const p = perms(nutzer.stufe2, 2)
const treffer = await sichtbar(p)
expect(treffer).not.toContain(ids.geheim)
expect(treffer).toContain(ids.offen)
// Auch die Zählung darf sie nicht enthalten, sonst verrät die Lücke sie.
const anzahl = await db.case.count({
where: { AND: [visibleCaseWhere(p), { id: { in: Object.values(ids) } }] },
})
expect(anzahl).toBe(treffer.length)
})
it('gibt einem ausgeschlossenen Nutzer mit Stufe 5 keinen Zugriff', async () => {
const treffer = await sichtbar(perms(nutzer.stufe5, 5))
expect(treffer).not.toContain(ids.offen)
})
it('schlägt selbst den Admin, wenn er ausgeschlossen ist', async () => {
// Der Ausschluss deckt den Fall ab, dass gegen einen hochrangigen
// Mitarbeiter ermittelt wird. Der Administrator ist einer.
const treffer = await sichtbar(perms(nutzer.admin, 5, { isAdmin: true }))
expect(treffer).not.toContain(ids.offen)
expect(treffer).toContain(ids.geheim)
})
it('lässt einen Ermittler auch über seiner Stufe lesen', async () => {
const treffer = await sichtbar(perms(nutzer.beteiligt, 1))
expect(treffer).toContain(ids.geheim)
})
it('verbirgt versiegelte Akten unterhalb von Stufe 5', async () => {
expect(await sichtbar(perms(nutzer.stufe2, 2))).not.toContain(ids.versiegelt)
expect(await sichtbar(perms(nutzer.stufe1, 1))).not.toContain(ids.versiegelt)
expect(await sichtbar(perms(nutzer.stufe5, 5))).toContain(ids.versiegelt)
})
it('verbirgt Verlaufseinträge oberhalb der Stufe des Lesers', async () => {
await db.caseEntry.createMany({
data: [
{
id: `${marke}-eintrag-offen`,
caseId: ids.offen,
type: 'NOTE',
body: 'Für alle lesbar',
authorId: nutzer.stufe1,
minClearance: 0,
},
{
id: `${marke}-eintrag-hoch`,
caseId: ids.offen,
type: 'NOTE',
body: 'Nur ab Stufe 4',
authorId: nutzer.stufe1,
minClearance: 4,
},
],
})
const p = perms(nutzer.stufe1, 1)
const eintraege = await db.caseEntry.findMany({
where: { AND: [visibleEntryWhere(p), { caseId: ids.offen }] },
select: { id: true },
})
const gefunden = eintraege.map((e) => e.id)
expect(gefunden).toContain(`${marke}-eintrag-offen`)
expect(gefunden).not.toContain(`${marke}-eintrag-hoch`)
})
it('öffnet die Akte nicht für Zeugen, Betroffene und Täter', async () => {
// Die Beteiligtenliste beantwortet zwei Fragen: wer bearbeitet den Fall,
// und um wen geht es darin. Nur die erste verschafft Zugang - sonst läse
// der Beschuldigte die Ermittlung gegen sich selbst mit.
const vorlage = await db.documentTemplate.findFirstOrThrow({
where: { key: { startsWith: marke } },
})
const akte = await db.case.create({
data: {
templateId: vorlage.id,
templateKey: vorlage.key,
createdById: nutzer.stufe5,
caseNumber: `Rol-2026-${Date.now().toString(36)}`,
title: 'Ermittlung',
classification: 4,
status: 'OPEN',
},
})
const p = perms(nutzer.stufe1, 1)
for (const rolle of ['WITNESS', 'SUBJECT', 'SUSPECT', 'ACCOMPLICE'] as const) {
await db.caseParticipant.create({
data: { caseId: akte.id, userId: nutzer.stufe1, role: rolle },
})
const sichtbar = await db.case.findFirst({
where: { AND: [visibleCaseWhere(p), { id: akte.id }] },
select: { id: true },
})
expect(sichtbar, `Rolle ${rolle} darf die Akte nicht öffnen`).toBeNull()
await db.caseParticipant.deleteMany({
where: { caseId: akte.id, userId: nutzer.stufe1, role: rolle },
})
}
// Gegenprobe: als Ermittler geht es, sonst prüft der Test nichts.
await db.caseParticipant.create({
data: { caseId: akte.id, userId: nutzer.stufe1, role: 'INVESTIGATOR' },
})
const alsErmittler = await db.case.findFirst({
where: { AND: [visibleCaseWhere(p), { id: akte.id }] },
select: { id: true },
})
expect(alsErmittler).not.toBeNull()
})
})