RET-04: worm-speicher-garantie-append-only
- archive/internal/wormstore.Store: Put schreibt einmalig (chmod 0400 danach, ErrAlreadyExists bei Ueberschreibversuch inkl. DB-seitiger Sperre gegen Wettlaufsituationen), GetVerified prueft SHA-256 bei jedem Zugriff, KEINE Delete-Funktion (strukturelle API-Grenze) - migrations/0007_worm_objects: append-only Metadatentabelle - AC2/Pruefung 3 vor Umsetzung praezisiert: Schutz ueber Go-API, kein absoluter Schutz gegen root (Nutzerentscheidung: kein chattr +i, nicht portabel/nicht ehrlich als absolut behauptbar) - Reflection-Test beweist strukturell: keine Loesch-Methode vorhanden - zusaetzlich echter Nachweis auf 131 als Nicht-Root-Betriebsnutzer (sudo -u nexarch): direkter Ueberschreibversuch scheitert real, Automatiktest selbst laeuft als root und uebersprang diesen Teil bewusst Pruefungen siehe archive/docs/RET-04-PRUEFPROTOKOLL.md
This commit is contained in:
@@ -0,0 +1,66 @@
|
|||||||
|
# RET-04 – Prüfprotokoll: WORM-Speicher-Garantie (Append-only)
|
||||||
|
|
||||||
|
Voraussetzung RET-01 – bereits Fertig.
|
||||||
|
|
||||||
|
## Grenzen des Schutzes (bewusst dokumentiert, nicht behauptet)
|
||||||
|
|
||||||
|
Nach Nutzerentscheidung (Option 1) beschränkt sich der technische Schutz
|
||||||
|
auf zwei Ebenen, beide EHRLICH begrenzt dokumentiert statt als absolut
|
||||||
|
verkauft:
|
||||||
|
|
||||||
|
1. **Dateisystem-Schreibschutz (`chmod 0400`)** — schützt zuverlässig
|
||||||
|
gegen den Betriebs-Nutzer der Dienste (`nexarch`, kein root), NICHT
|
||||||
|
gegen root/CAP_DAC_OVERRIDE. Ein `chattr +i`-Immutable-Bit wurde
|
||||||
|
BEWUSST NICHT eingesetzt (Nutzerentscheidung): nicht portabel über
|
||||||
|
alle Dateisysteme, braucht root-Capabilities, von einem anderen
|
||||||
|
root-Prozess ebenso umgehbar — hätte einen falschen Eindruck von
|
||||||
|
Absolutheit erzeugt.
|
||||||
|
2. **Keine Delete-Funktion in der Go-API** — struktureller Schutz auf
|
||||||
|
Anwendungsebene: kein Code-Pfad in `wormstore` kann ein Objekt
|
||||||
|
löschen. Root-OS-Zugriff (`rm`) bleibt außerhalb der Kontrolle jeder
|
||||||
|
Anwendung, das ist explizit im Package-Kommentar und hier
|
||||||
|
dokumentiert, nicht verschwiegen.
|
||||||
|
|
||||||
|
Board-AC2 und Prüfung 3 wurden vor der Umsetzung entsprechend
|
||||||
|
präzisiert ("technisch unterbunden" → "über die Go-API technisch
|
||||||
|
unmöglich, kein absoluter Schutz gegen root").
|
||||||
|
|
||||||
|
## Umsetzung
|
||||||
|
|
||||||
|
- `archive/migrations/0007_worm_objects.up/down.sql` – `worm_objects`
|
||||||
|
(storage_key PK, checksum_sha256, size_bytes, written_at) — append-only
|
||||||
|
Metadaten, kein UPDATE-Pfad im Code.
|
||||||
|
- `archive/internal/wormstore.Store`:
|
||||||
|
- `Put` – schreibt einmalig, `ErrAlreadyExists` bei zweitem Versuch
|
||||||
|
(sowohl Datei- als auch DB-Ebene, inkl. Wettlaufsituation über
|
||||||
|
`ON CONFLICT DO NOTHING` + `RowsAffected`-Check), setzt danach
|
||||||
|
`chmod 0400`.
|
||||||
|
- `GetVerified` – liest und vergleicht die SHA-256-Prüfsumme bei
|
||||||
|
JEDEM Zugriff gegen den beim Schreiben gespeicherten Wert.
|
||||||
|
- **Kein `Delete`** — bewusst, siehe oben.
|
||||||
|
|
||||||
|
## Prüfungen
|
||||||
|
|
||||||
|
| # | Prüfung | Ergebnis |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | Direkter Überschreibversuch am Speicherpfad wird abgewiesen | **bestanden** – `TestPut_DirectOverwriteAttemptIsRejected`: API-Ebene (`ErrAlreadyExists`) real getestet; zusätzlich ECHTER Nachweis auf 131 als Nicht-Root-Nutzer (`sudo -u nexarch`, dem tatsächlichen Betriebsnutzer der Dienste): direkter Schreibversuch auf eine `chmod 0400`-Datei schlägt real mit "Permission denied" fehl, Inhalt bleibt unverändert. Der Automatik-Test selbst läuft auf 131 als root (`os.Geteuid()==0`) und übersprang den OS-Teilnachweis dort bewusst (root umgeht Unix-Rechte strukturell) — deshalb der zusätzliche manuelle Nachweis als echter Nicht-Root-Nutzer |
|
||||||
|
| 2 | Prüfsummenabgleich erkennt eine manipulierte Testdatei zuverlässig | **bestanden** – `TestGetVerified_DetectsTamperedFile`: Datei nach dem Schreiben real manipuliert (Schreibschutz testweise aufgehoben, Inhalt verändert), `GetVerified` liefert real `ErrChecksumMismatch` |
|
||||||
|
| 3 | Löschung außerhalb des Workflows ist technisch unterbunden (präzisiert: über die Go-API), nicht nur per Konvention | **bestanden** – `TestStore_HasNoDeleteMethod`: Reflection-Test beweist strukturell, dass `wormstore.Store` KEINE Lösch-Methode besitzt — kein Kommentar/keine Konvention, sondern eine compile-time-nachweisbare API-Grenze |
|
||||||
|
|
||||||
|
## Build/Test-Ergebnis (192.168.1.131)
|
||||||
|
|
||||||
|
```
|
||||||
|
go build ./... -> clean
|
||||||
|
go vet ./... -> clean
|
||||||
|
golangci-lint run ./... -> 0 issues
|
||||||
|
go test ./... -p 1 -> alle Archive-Pakete bestanden (inkl. wormstore)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gesamtergebnis
|
||||||
|
|
||||||
|
**Bestanden.** Alle drei (vor Umsetzung ehrlich präzisierten)
|
||||||
|
Akzeptanzkriterien und Pflichtprüfungen real erfüllt — inklusive eines
|
||||||
|
zusätzlichen, außerplanmäßigen manuellen Nachweises als echter
|
||||||
|
Nicht-Root-Betriebsnutzer, weil der automatisierte Test auf 131 als
|
||||||
|
root lief und den entscheidenden OS-Teilnachweis sonst nicht ehrlich
|
||||||
|
hätte erbringen können.
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
// Package wormstore implementiert RET-04: einen Speicherpfad für
|
||||||
|
// archivierte Objektinhalte, der nach dem Schreiben weder überschrieben
|
||||||
|
// noch (über dieses Paket) gelöscht werden kann — Write Once, Read Many.
|
||||||
|
//
|
||||||
|
// Löschung: dieses Paket bietet BEWUSST KEINE Delete-Funktion — das ist
|
||||||
|
// die technische Grenze, die Go-Anwendungscode überhaupt durchsetzen
|
||||||
|
// kann. Direkter Zugriff auf das Dateisystem durch einen Prozess mit
|
||||||
|
// root-Rechten bleibt außerhalb der Kontrolle jeder Anwendung; das ist
|
||||||
|
// ein struktureller Schutz auf Anwendungsebene, kein absoluter Schutz
|
||||||
|
// gegen root-OS-Zugriff (siehe RET-04-Prüfprotokoll). Der kontrollierte
|
||||||
|
// Löschworkflow (RET-03, eigenes Ticket) bekommt bei Bedarf einen
|
||||||
|
// separaten, explizit privilegierten Zugriffspfad — NICHT Teil dieses
|
||||||
|
// Pakets.
|
||||||
|
package wormstore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrAlreadyExists wird geliefert, wenn unter demselben Schlüssel bereits
|
||||||
|
// ein Objekt geschrieben wurde (Akzeptanzkriterium 1: kein Überschreiben).
|
||||||
|
var ErrAlreadyExists = errors.New("wormstore: objekt existiert bereits, überschreiben nicht erlaubt")
|
||||||
|
|
||||||
|
// ErrNotFound wird geliefert, wenn ein angefragtes Objekt nicht existiert.
|
||||||
|
var ErrNotFound = errors.New("wormstore: objekt nicht gefunden")
|
||||||
|
|
||||||
|
// ErrChecksumMismatch wird von GetVerified geliefert, wenn der beim Lesen
|
||||||
|
// berechnete Hash nicht mit dem beim Schreiben gespeicherten übereinstimmt
|
||||||
|
// (Akzeptanzkriterium 3: manipulierter Inhalt wird erkannt).
|
||||||
|
var ErrChecksumMismatch = errors.New("wormstore: prüfsumme stimmt nicht überein — inhalt wurde verändert")
|
||||||
|
|
||||||
|
// Store ist der WORM-Speicher: Dateien im lokalen Dateisystem, Metadaten
|
||||||
|
// (Prüfsumme, Größe) in Postgres — beide append-only.
|
||||||
|
type Store struct {
|
||||||
|
baseDir string
|
||||||
|
pool *pgxpool.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(baseDir string, pool *pgxpool.Pool) *Store {
|
||||||
|
return &Store{baseDir: baseDir, pool: pool}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) path(key string) string {
|
||||||
|
return filepath.Join(s.baseDir, filepath.FromSlash(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put schreibt r EINMALIG unter key. Existiert key bereits (Datei ODER
|
||||||
|
// DB-Zeile), wird ErrAlreadyExists geliefert — kein Überschreiben, auch
|
||||||
|
// nicht bei gleichem Inhalt. Nach erfolgreichem Schreiben wird die Datei
|
||||||
|
// read-only gesetzt (0400): ein direkter os.OpenFile mit Schreibabsicht
|
||||||
|
// schlägt danach mit "permission denied" fehl (Akzeptanzkriterium 1,
|
||||||
|
// zusätzlich zur DB-seitigen Sperre über den PRIMARY KEY).
|
||||||
|
func (s *Store) Put(ctx context.Context, key string, r io.Reader) (checksum string, size int64, err error) {
|
||||||
|
full := s.path(key)
|
||||||
|
if _, statErr := os.Stat(full); statErr == nil {
|
||||||
|
return "", 0, ErrAlreadyExists
|
||||||
|
} else if !os.IsNotExist(statErr) {
|
||||||
|
return "", 0, fmt.Errorf("wormstore: existenz prüfen: %w", statErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
|
||||||
|
return "", 0, fmt.Errorf("wormstore: verzeichnis anlegen: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp := full + ".tmp"
|
||||||
|
f, err := os.OpenFile(tmp, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, fmt.Errorf("wormstore: temporäre datei anlegen: %w", err)
|
||||||
|
}
|
||||||
|
hasher := sha256.New()
|
||||||
|
written, err := io.Copy(io.MultiWriter(f, hasher), r)
|
||||||
|
if err != nil {
|
||||||
|
_ = f.Close()
|
||||||
|
_ = os.Remove(tmp)
|
||||||
|
return "", 0, fmt.Errorf("wormstore: schreiben: %w", err)
|
||||||
|
}
|
||||||
|
if err := f.Close(); err != nil {
|
||||||
|
_ = os.Remove(tmp)
|
||||||
|
return "", 0, fmt.Errorf("wormstore: datei schließen: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Rename(tmp, full); err != nil {
|
||||||
|
_ = os.Remove(tmp)
|
||||||
|
return "", 0, fmt.Errorf("wormstore: atomar platzieren: %w", err)
|
||||||
|
}
|
||||||
|
// Read-only NACH dem Schließen — verhindert jedes weitere Schreiben
|
||||||
|
// (Akzeptanzkriterium 1), auch durch denselben Prozess.
|
||||||
|
if err := os.Chmod(full, 0o400); err != nil {
|
||||||
|
return "", 0, fmt.Errorf("wormstore: schreibschutz setzen: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sum := hex.EncodeToString(hasher.Sum(nil))
|
||||||
|
tag, err := s.pool.Exec(ctx, `
|
||||||
|
INSERT INTO worm_objects (storage_key, checksum_sha256, size_bytes)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
ON CONFLICT (storage_key) DO NOTHING
|
||||||
|
`, key, sum, written)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, fmt.Errorf("wormstore: metadaten speichern: %w", err)
|
||||||
|
}
|
||||||
|
if tag.RowsAffected() == 0 {
|
||||||
|
// Datei wurde gerade erst neu geschrieben (s.o. Stat-Check), aber
|
||||||
|
// die Metadaten-Zeile existierte bereits — Wettlaufsituation
|
||||||
|
// zweier gleichzeitiger Put-Aufrufe. Kein Überschreiben zulassen.
|
||||||
|
return "", 0, ErrAlreadyExists
|
||||||
|
}
|
||||||
|
|
||||||
|
return sum, written, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVerified liefert den Inhalt UND vergleicht bei JEDEM Lesezugriff die
|
||||||
|
// Prüfsumme gegen die beim Schreiben gespeicherte (Akzeptanzkriterium 3).
|
||||||
|
func (s *Store) GetVerified(ctx context.Context, key string) ([]byte, error) {
|
||||||
|
var expectedSum string
|
||||||
|
err := s.pool.QueryRow(ctx, `SELECT checksum_sha256 FROM worm_objects WHERE storage_key = $1`, key).Scan(&expectedSum)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("wormstore: metadaten lesen: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(s.path(key))
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("wormstore: datei lesen: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sum := sha256.Sum256(data)
|
||||||
|
actualSum := hex.EncodeToString(sum[:])
|
||||||
|
if actualSum != expectedSum {
|
||||||
|
return nil, ErrChecksumMismatch
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package wormstore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupTest(t *testing.T) (*Store, *pgxpool.Pool) {
|
||||||
|
t.Helper()
|
||||||
|
dsn := os.Getenv("TEST_TENANT_DSN")
|
||||||
|
if dsn == "" {
|
||||||
|
t.Skip("TEST_TENANT_DSN nicht gesetzt, Integrationstest uebersprungen")
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
pool, err := pgxpool.New(ctx, dsn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("pool: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { pool.Close() })
|
||||||
|
|
||||||
|
if _, err := pool.Exec(ctx, `
|
||||||
|
CREATE TABLE IF NOT EXISTS worm_objects (
|
||||||
|
storage_key TEXT PRIMARY KEY, checksum_sha256 TEXT NOT NULL,
|
||||||
|
size_bytes BIGINT NOT NULL, written_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
`); err != nil {
|
||||||
|
t.Fatalf("schema: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_, _ = pool.Exec(context.Background(), `DELETE FROM worm_objects WHERE storage_key LIKE 'wormtest_%'`)
|
||||||
|
})
|
||||||
|
|
||||||
|
return New(t.TempDir(), pool), pool
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPut_DirectOverwriteAttemptIsRejected ist die geforderte
|
||||||
|
// Pflichtprüfung 1: Objekt mit aktivem Schreibschutz widersteht einem
|
||||||
|
// direkten Überschreibversuch — sowohl über die Store-API als auch
|
||||||
|
// direkt am Dateisystem (echter Nachweis der Datei-Schreibsperre, nicht
|
||||||
|
// nur der Anwendungslogik).
|
||||||
|
func TestPut_DirectOverwriteAttemptIsRejected(t *testing.T) {
|
||||||
|
store, _ := setupTest(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
sum1, size1, err := store.Put(ctx, "wormtest_a", strings.NewReader("erster inhalt"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("erster put: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Überschreibversuch über die API.
|
||||||
|
_, _, err = store.Put(ctx, "wormtest_a", strings.NewReader("manipulierter inhalt"))
|
||||||
|
if !errors.Is(err, ErrAlreadyExists) {
|
||||||
|
t.Fatalf("erwartet ErrAlreadyExists beim zweiten put, habe: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direkter Überschreibversuch am Dateisystem (umgeht die API) — muss
|
||||||
|
// an der echten OS-Schreibsperre (chmod 0400) scheitern. Nur
|
||||||
|
// aussagekräftig, wenn NICHT als root getestet wird: root umgeht
|
||||||
|
// Unix-Dateirechte grundsätzlich (DAC-Bypass), das ist kein Defekt
|
||||||
|
// dieses Pakets, sondern eine bekannte Grenze von chmod-basiertem
|
||||||
|
// Schutz (siehe Paket-Dokumentation "kein absoluter Schutz gegen
|
||||||
|
// root"). Auf 131 laufen Tests als root — dieser Teilnachweis wird
|
||||||
|
// dort bewusst übersprungen, die API-seitige Sperre (oben) bleibt
|
||||||
|
// die primäre, root-unabhängige Prüfung.
|
||||||
|
if os.Geteuid() != 0 {
|
||||||
|
full := store.path("wormtest_a")
|
||||||
|
f, openErr := os.OpenFile(full, os.O_WRONLY, 0)
|
||||||
|
if openErr == nil {
|
||||||
|
_ = f.Close()
|
||||||
|
t.Fatal("erwartet permission-fehler beim direkten öffnen zum schreiben, datei war schreibbar")
|
||||||
|
}
|
||||||
|
if !os.IsPermission(openErr) {
|
||||||
|
t.Fatalf("erwartet permission-fehler, habe: %v", openErr)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Log("laeuft als root: OS-Schreibsperren-Teilnachweis uebersprungen (root umgeht Unix-Dateirechte grundsaetzlich)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ursprünglicher Inhalt unverändert.
|
||||||
|
data, err := store.GetVerified(ctx, "wormtest_a")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("getverified: %v", err)
|
||||||
|
}
|
||||||
|
if string(data) != "erster inhalt" {
|
||||||
|
t.Fatalf("inhalt wurde veraendert: %q", data)
|
||||||
|
}
|
||||||
|
if size1 != int64(len("erster inhalt")) || sum1 == "" {
|
||||||
|
t.Fatalf("unerwartete put-metadaten: sum=%q size=%d", sum1, size1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGetVerified_DetectsTamperedFile ist die geforderte Pflichtprüfung
|
||||||
|
// 2: Prüfsummenabgleich erkennt eine manipulierte Testdatei zuverlässig.
|
||||||
|
func TestGetVerified_DetectsTamperedFile(t *testing.T) {
|
||||||
|
store, _ := setupTest(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
if _, _, err := store.Put(ctx, "wormtest_b", strings.NewReader("unveraenderter inhalt")); err != nil {
|
||||||
|
t.Fatalf("put: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manipulation simulieren: Schreibschutz temporär aufheben (root/
|
||||||
|
// Betreiber-Zugriff außerhalb der API, genau der Fall, den die
|
||||||
|
// Prüfsumme abfangen soll) und den Inhalt verändern.
|
||||||
|
full := store.path("wormtest_b")
|
||||||
|
if err := os.Chmod(full, 0o600); err != nil {
|
||||||
|
t.Fatalf("chmod fuer testmanipulation: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(full, []byte("MANIPULIERTER INHALT"), 0o600); err != nil {
|
||||||
|
t.Fatalf("testmanipulation schreiben: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := store.GetVerified(ctx, "wormtest_b")
|
||||||
|
if !errors.Is(err, ErrChecksumMismatch) {
|
||||||
|
t.Fatalf("erwartet ErrChecksumMismatch bei manipulierter datei, habe: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStore_HasNoDeleteMethod ist die geforderte Pflichtprüfung 3
|
||||||
|
// (präzisiert): Löschung ist über die Go-API des Pakets technisch
|
||||||
|
// unmöglich, weil KEINE Delete-Funktion existiert — strukturell per
|
||||||
|
// Reflection nachgewiesen, kein bloßer Verzicht per Konvention/Kommentar.
|
||||||
|
func TestStore_HasNoDeleteMethod(t *testing.T) {
|
||||||
|
storeType := reflect.TypeOf(&Store{})
|
||||||
|
for i := 0; i < storeType.NumMethod(); i++ {
|
||||||
|
name := storeType.Method(i).Name
|
||||||
|
if strings.Contains(strings.ToLower(name), "delete") || strings.Contains(strings.ToLower(name), "remove") {
|
||||||
|
t.Fatalf("wormstore.Store darf keine Lösch-Methode besitzen, gefunden: %s", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS worm_objects;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
-- RET-04: WORM-Speicher-Garantie. Append-only Metadatentabelle - kein
|
||||||
|
-- UPDATE-Pfad im Code, PRIMARY KEY(storage_key) verhindert eine zweite
|
||||||
|
-- Zeile fuer denselben Schluessel (Akzeptanzkriterium 1, zusaetzlich zur
|
||||||
|
-- Dateisystem-Schreibsperre).
|
||||||
|
CREATE TABLE IF NOT EXISTS worm_objects (
|
||||||
|
storage_key TEXT PRIMARY KEY,
|
||||||
|
checksum_sha256 TEXT NOT NULL,
|
||||||
|
size_bytes BIGINT NOT NULL,
|
||||||
|
written_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user