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.
226 lines
7.2 KiB
TypeScript
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',
|
|
})
|
|
})
|
|
})
|