Serverseitige Verschluesselung optional machen

Der S3-Treiber setzte bei jedem PutObject fest
ServerSideEncryption: 'AES256'. Das mitgelieferte MinIO laeuft ohne KMS
und lehnt das mit "NotImplemented" ab - es liess sich kein einziges
Dokument ablegen. Wer der INSTALLATION.md folgte, landete zwangslaeufig
dort; es war kein Konfigurationsfehler des Betreibers, sondern der
Auslieferungszustand.

Gebraucht wird die Zeile ohnehin nicht mehr. Seit v1.11.0 verschluesselt
die Anwendung jede Datei selbst, bevor sie den Server verlaesst. Eine
zweite Lage durch den Anbieter schuetzt vor nichts, was der eigene
Umschlag nicht schon abdeckt.

Jetzt steuert S3_SSE, ob der Kopf mitgeht. Leer bedeutet aus - auch eine
Zeile mit Leerzeichen, sonst schickte eine leergelassene .env-Zeile einen
leeren Kopf und der Anbieter antwortete mit einem Fehler, den niemand
versteht.

Im Kommentar von v1.11.0 stand, die serverseitige Verschluesselung
"kostet nichts". Das war falsch: Auf MinIO ohne KMS kostete sie die
gesamte Funktion. Der Satz ist korrigiert.

Aufgefallen ist das erst im Betrieb, und der Grund ist derselbe wie beim
Ausloeser: In der Entwicklung laeuft der Dateitreiber, der S3-Pfad wurde
von keinem Test ausgefuehrt. Deshalb pruefen die neuen Tests den Befehl
ab, bevor er das Netz sieht - was hineingeschrieben wird, ist jetzt
belegt, nicht angenommen.

Gegen drei Mutationen geprueft: SSE wieder fest verdrahtet, leere Angabe
nicht abgefangen, Inhalt unverschluesselt - jede wird rot.

Refs #42
This commit is contained in:
Florian Wilde
2026-09-01 16:08:26 +02:00
parent dbc24d965d
commit c0cb29bf53
5 changed files with 187 additions and 4 deletions
+136
View File
@@ -0,0 +1,136 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
/**
* Was der S3-Treiber tatsächlich an den Objektspeicher schickt.
*
* Anlass: Der Treiber setzte bei jedem PutObject fest
* `ServerSideEncryption: 'AES256'`. Das mitgelieferte MinIO läuft ohne KMS
* und lehnte das mit `NotImplemented` ab — es ließ sich kein einziges
* Dokument ablegen, und wer der INSTALLATION.md folgte, landete
* zwangsläufig dort.
*
* Gemerkt hat das kein Test, weil in der Entwicklung der Dateitreiber
* läuft und der S3-Pfad nie ausgeführt wurde. Diese Datei schließt genau
* diese Lücke: Sie fängt den Befehl ab, bevor er das Netz sieht, und
* schaut hinein.
*/
process.env.S3_ENDPOINT = 'http://minio:9000'
process.env.S3_BUCKET = 'betreuungs-cockpit'
process.env.S3_ACCESS_KEY_ID = 'probe-zugang'
process.env.S3_SECRET_ACCESS_KEY = 'probe-geheimnis'
/** Der letzte Befehl, den der Treiber abgeschickt hat. */
let letzterBefehl: Record<string, unknown> | null = null
vi.mock('@aws-sdk/client-s3', () => {
class PutObjectCommand {
input: Record<string, unknown>
constructor(input: Record<string, unknown>) {
this.input = input
}
}
class GetObjectCommand extends PutObjectCommand {}
class DeleteObjectCommand extends PutObjectCommand {}
class HeadObjectCommand extends PutObjectCommand {}
class ListObjectsV2Command extends PutObjectCommand {}
class S3Client {
async send(befehl: { input: Record<string, unknown> }) {
letzterBefehl = befehl.input
return {}
}
}
return {
S3Client,
PutObjectCommand,
GetObjectCommand,
DeleteObjectCommand,
HeadObjectCommand,
ListObjectsV2Command,
}
})
const SCHLUESSEL = 'betreute/cmt4ey0jm0003o001wq4rc3m9/dokumente/ba7a8c1a-d4c5-41c7-8f5f-ebfb4805b0e5.pdf'
const INHALT = Buffer.from('%PDF-1.4 ein Bescheid', 'utf8')
/**
* Der Treiber baut seinen Klienten einmal und merkt ihn sich. Damit die
* Umgebungsvariable je Testfall neu gelesen wird, muss das Modul frisch
* geladen werden.
*/
async function treiberFrisch() {
vi.resetModules()
return import('@/lib/speicher/s3')
}
describe('S3-Treiber: serverseitige Verschlüsselung', () => {
beforeEach(() => {
letzterBefehl = null
delete process.env.S3_SSE
})
afterEach(() => {
delete process.env.S3_SSE
})
it('schickt ohne S3_SSE keinen Verschlüsselungskopf mit', async () => {
const s3 = await treiberFrisch()
await s3.schreiben(SCHLUESSEL, INHALT, 'application/pdf')
expect(letzterBefehl).not.toBeNull()
expect(letzterBefehl).not.toHaveProperty('ServerSideEncryption')
})
it('schickt ihn mit, wenn S3_SSE gesetzt ist', async () => {
process.env.S3_SSE = 'AES256'
const s3 = await treiberFrisch()
await s3.schreiben(SCHLUESSEL, INHALT, 'application/pdf')
expect(letzterBefehl?.ServerSideEncryption).toBe('AES256')
})
it('übernimmt auch einen anderen Wert, den ein Anbieter verlangt', async () => {
process.env.S3_SSE = 'aws:kms'
const s3 = await treiberFrisch()
await s3.schreiben(SCHLUESSEL, INHALT, 'application/pdf')
expect(letzterBefehl?.ServerSideEncryption).toBe('aws:kms')
})
it('behandelt eine leere Angabe wie "aus"', async () => {
// Sonst schickte eine leergelassene Zeile in der .env einen leeren
// Kopf, und der Anbieter antwortete mit einem Fehler, den niemand
// versteht.
process.env.S3_SSE = ' '
const s3 = await treiberFrisch()
await s3.schreiben(SCHLUESSEL, INHALT, 'application/pdf')
expect(letzterBefehl).not.toHaveProperty('ServerSideEncryption')
})
})
describe('S3-Treiber: was sonst noch mitgeht', () => {
beforeEach(() => {
letzterBefehl = null
delete process.env.S3_SSE
})
it('legt den Inhalt verschlüsselt ab, nicht im Klartext', async () => {
const s3 = await treiberFrisch()
await s3.schreiben(SCHLUESSEL, INHALT, 'application/pdf')
const koerper = letzterBefehl?.Body as Buffer
expect(koerper.subarray(0, 4).toString('ascii')).toBe('BCD1')
expect(koerper.includes(Buffer.from('ein Bescheid', 'utf8'))).toBe(false)
})
it('meldet den Inhaltstyp nicht als PDF - dort liegt Geheimtext', async () => {
const s3 = await treiberFrisch()
await s3.schreiben(SCHLUESSEL, INHALT, 'application/pdf')
expect(letzterBefehl?.ContentType).toBe('application/octet-stream')
expect(letzterBefehl?.ContentDisposition).toBe('attachment')
})
})