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
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die README; an keiner Stelle Bedeutung, nur Zeichensetzung.
239 lines
7.8 KiB
TypeScript
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()
|
|
})
|
|
})
|