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

226 lines
7.2 KiB
TypeScript

// @vitest-environment node
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { createPrismaClient, loadEnvFromRepoRoot, type PrismaClient } from '@afis/db'
import { visibleDocumentWhere } from '@/lib/access/personnelFilter'
import {
canCreateDocument,
canEditDocument,
canRetractDocument,
} from '@/lib/access/documentWrite'
import type { EffectivePermissions, Permission } from '@/lib/auth/permissions'
/**
* Dokumente erben die Sichtbarkeit ihres Besitzers **und** haben eine eigene.
* Beides muss zutreffen, sonst leckt entweder die Existenz der Akte oder der
* Inhalt des Dokuments.
*/
loadEnvFromRepoRoot()
const url = process.env.DIRECT_DATABASE_URL ?? process.env.DATABASE_URL
const suite = url ? describe : describe.skip
let db: PrismaClient
const marke = `dok-${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 Dokumenten', () => {
const nutzer = {
stufe1: `${marke}-stufe1`,
stufe4: `${marke}-stufe4`,
beteiligt: `${marke}-beteiligt`,
}
const dok = {
imOffenenFall: '',
hochImOffenenFall: '',
imGeheimenFall: '',
anVerborgenerPerson: '',
freistehend: '',
}
beforeAll(async () => {
db = createPrismaClient(url)
const template = await db.documentTemplate.create({
data: {
key: `${marke}-vorlage`,
version: 1,
name: 'Prüfvorlage',
fields: [],
uses: ['CASE_DOCUMENT', 'PERSON_DOCUMENT', 'STANDALONE'],
},
})
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.stufe4,
}
const offen = await db.case.create({
data: {
...gemeinsam,
caseNumber: `${marke}-offen`.slice(0, 40),
title: 'Offen',
classification: 1,
status: 'OPEN',
},
})
const geheim = await db.case.create({
data: {
...gemeinsam,
caseNumber: `${marke}-geheim`.slice(0, 40),
title: 'Geheim',
classification: 4,
status: 'OPEN',
},
})
await db.caseParticipant.create({
data: { caseId: geheim.id, userId: nutzer.beteiligt, role: 'INVESTIGATOR' },
})
const person = await db.personnelFile.create({
data: { displayName: `${marke} Verborgen`, classification: 4 },
})
const anlegen = async (
nummer: string,
classification: number,
besitzer: { caseId?: string; personnelFileId?: string },
) =>
(
await db.document.create({
data: {
documentNumber: `${marke}-${nummer}`,
title: nummer,
templateId: template.id,
templateKey: template.key,
templateVersion: 1,
classification,
createdById: nutzer.stufe4,
...besitzer,
},
select: { id: true },
})
).id
dok.imOffenenFall = await anlegen('offen-niedrig', 1, { caseId: offen.id })
dok.hochImOffenenFall = await anlegen('offen-hoch', 4, { caseId: offen.id })
dok.imGeheimenFall = await anlegen('geheim', 1, { caseId: geheim.id })
dok.anVerborgenerPerson = await anlegen('person', 1, { personnelFileId: person.id })
dok.freistehend = await anlegen('frei', 1, {})
})
afterAll(async () => {
await db.$disconnect()
})
async function sichtbar(p: EffectivePermissions): Promise<string[]> {
const rows = await db.document.findMany({
where: { AND: [visibleDocumentWhere(p), { id: { in: Object.values(dok) } }] },
select: { id: true },
})
return rows.map((r) => r.id).sort()
}
it('zeigt einer Stufe 1 nur, was Besitzer und Dokument zugleich erlauben', async () => {
const p = perms(nutzer.stufe1, 1)
const treffer = await sichtbar(p)
expect(treffer).toContain(dok.imOffenenFall)
expect(treffer).toContain(dok.freistehend)
// Das Dokument ist zu hoch eingestuft, obwohl die Akte offen liegt.
expect(treffer).not.toContain(dok.hochImOffenenFall)
// Das Dokument wäre lesbar, aber die Akte darüber nicht - die
// Dokumentenliste verriete sonst ihre Existenz.
expect(treffer).not.toContain(dok.imGeheimenFall)
expect(treffer).not.toContain(dok.anVerborgenerPerson)
const anzahl = await db.document.count({
where: { AND: [visibleDocumentWhere(p), { id: { in: Object.values(dok) } }] },
})
expect(anzahl).toBe(treffer.length)
})
it('lässt einen Beteiligten die Dokumente seiner Akte lesen', async () => {
// Ausnahmsweiser Zugang gilt auch für die Dokumente der Akte, sonst wäre
// das Hinzuziehen wirkungslos. Siehe docs/03-auth-und-rechte.md.
const treffer = await sichtbar(perms(nutzer.beteiligt, 1))
expect(treffer).toContain(dok.imGeheimenFall)
// Er bleibt aber ein Stufe-1-Nutzer: an fremden Personalakten ändert das nichts.
expect(treffer).not.toContain(dok.anVerborgenerPerson)
})
it('zeigt der Stufe 4 alles', async () => {
expect(await sichtbar(perms(nutzer.stufe4, 4))).toHaveLength(Object.values(dok).length)
})
})
describe('Schreibgrenzen für Dokumente', () => {
const alle: Permission[] = [
'document.create',
'document.edit',
'document.finalize',
'document.retract',
]
it('lässt niemanden ein Dokument über der eigenen Stufe anlegen', () => {
const p = perms('a', 2, { permissions: alle })
expect(canCreateDocument(p, 2).ok).toBe(true)
expect(canCreateDocument(p, 3)).toEqual({
ok: false,
reason: 'clearance-too-low',
required: 3,
})
})
it('verlangt für das Ändern eines ausgefertigten Dokuments das Ausfertigungsrecht', () => {
const nurEdit = perms('b', 5, { permissions: ['document.edit'] })
expect(canEditDocument(nurEdit, { status: 'DRAFT', classification: 1 }).ok).toBe(true)
expect(canEditDocument(nurEdit, { status: 'FINAL', classification: 1 })).toEqual({
ok: false,
reason: 'closed',
})
const mitFinalize = perms('c', 5, { permissions: ['document.edit', 'document.finalize'] })
expect(canEditDocument(mitFinalize, { status: 'FINAL', classification: 1 }).ok).toBe(true)
})
it('hält ein zurückgezogenes Dokument auch für den Admin geschlossen', () => {
// Der Grabstein bleibt stehen, wie er ist. Sonst liesse sich ein
// zurückgezogenes Dokument nachträglich zu etwas anderem machen.
const admin = perms('d', 5, { isAdmin: true })
expect(canEditDocument(admin, { status: 'RETRACTED', classification: 1 })).toEqual({
ok: false,
reason: 'read-only',
status: 'RETRACTED',
})
expect(canRetractDocument(admin, { status: 'RETRACTED', classification: 1 }).ok).toBe(false)
})
it('verlangt für das Zurückziehen ein eigenes Recht', () => {
const ohne = perms('e', 5, { permissions: ['document.edit'] })
expect(canRetractDocument(ohne, { status: 'FINAL', classification: 1 })).toEqual({
ok: false,
reason: 'no-permission',
permission: 'document.retract',
})
})
})