Files
deklarix/internal/store/loeschfrist.go
noroot fe28278615 feat: Row-Level-Security für Mandantenisolation auf DB-Ebene
Postgres-RLS-Policies auf allen Tabellen mit echten Mandanten-
Geschäftsdaten (antrag und alles darüber verkettete, abteilung,
werkzeug/werkzeug_sperre, genehmiger_rolle/freigabe_regel,
loeschfrist_einstellung). Kritischer Fund vor der Umsetzung: die
Anwendung verbindet als postgres-Superuser, der RLS immer umgeht -
Migration 0021 legt deshalb zusätzlich eine eingeschränkte Rolle
"deklarix_app" an, nur für die greifen die Policies tatsächlich.

internal/store/tenant_scope.go: WithTenantScope öffnet eine Transaktion
und setzt Sitzungsvariablen (app.account_id/app.is_betreiber) per
set_config mit Parameterbindung; alle Store-Methoden laufen jetzt über
s.db(ctx) statt direkt s.Pool. Jede require*-Middleware umschließt die
komplette Handler-Ausführung damit - jeder Request läuft dadurch auch
atomar in einer Transaktion (positiver Nebeneffekt).

Live end-to-end verifiziert (echter HTTP-Server + DATABASE_URL_APP auf
die eingeschränkte Rolle gesetzt): zwei Firmen registriert, Isolation
über Abteilung/Antrag/Bewertung bestätigt, zentraler NULL-Katalog für
beide sichtbar. Produktivbetrieb braucht noch einen manuellen Schritt
(Passwort für deklarix_app setzen + DATABASE_URL_APP konfigurieren,
siehe CLAUDE.md) - die Migration allein aktiviert noch nichts, solange
die App weiter als Superuser verbindet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-01 09:22:20 +02:00

70 lines
2.6 KiB
Go

// Löschfristen je Datenklasse (Migration 0020) — schließt die in
// rules/OPEN.md, Punkt 4 dokumentierte Lücke: loeschfrist_max_tage war
// bisher nur als Anforderung "vorhanden", ohne Tageswerte, und wurde
// deshalb nicht hart gefiltert. Die konkreten Tageswerte sind KEINE
// gesetzliche Vorgabe (die DSGVO nennt keine festen Fristen, nur den
// Grundsatz "so lange wie für den Zweck nötig", Art. 5 Abs. 1 lit. e) —
// deshalb pro Mandant einstellbar statt hartkodiert, vom
// Datenschutzbeauftragten hinterlegt. Bei Firmenanlage werden
// risikogestaffelte Vorschlagswerte vorbelegt (siehe
// internal/web/loeschfrist_handlers.go), frei editierbar.
package store
import (
"context"
"fmt"
"time"
)
type LoeschfristEinstellung struct {
ID string
AccountID string
DatenklasseID string
MaxTage int
UpdatedAt time.Time
}
// UpsertLoeschfristEinstellung legt eine Einstellung an oder
// aktualisiert sie — ein Mandant hat höchstens eine Frist je
// Datenklasse (UNIQUE-Constraint).
func (s *Store) UpsertLoeschfristEinstellung(ctx context.Context, accountID, datenklasseID string, maxTage int) (LoeschfristEinstellung, error) {
var e LoeschfristEinstellung
err := s.db(ctx).QueryRow(ctx, `
INSERT INTO loeschfrist_einstellung (account_id, datenklasse_id, max_tage)
VALUES ($1, $2, $3)
ON CONFLICT (account_id, datenklasse_id)
DO UPDATE SET max_tage = $3, updated_at = now()
RETURNING id, account_id, datenklasse_id, max_tage, updated_at
`, accountID, datenklasseID, maxTage).Scan(&e.ID, &e.AccountID, &e.DatenklasseID, &e.MaxTage, &e.UpdatedAt)
if err != nil {
return LoeschfristEinstellung{}, fmt.Errorf("store: upsert loeschfrist einstellung: %w", err)
}
return e, nil
}
// ListLoeschfristEinstellungenForAccount liefert alle konfigurierten
// Fristen eines Mandanten.
func (s *Store) ListLoeschfristEinstellungenForAccount(ctx context.Context, accountID string) ([]LoeschfristEinstellung, error) {
rows, err := s.db(ctx).Query(ctx, `
SELECT id, account_id, datenklasse_id, max_tage, updated_at
FROM loeschfrist_einstellung WHERE account_id = $1 ORDER BY datenklasse_id
`, accountID)
if err != nil {
return nil, fmt.Errorf("store: list loeschfrist einstellungen: %w", err)
}
defer rows.Close()
var out []LoeschfristEinstellung
for rows.Next() {
var e LoeschfristEinstellung
if err := rows.Scan(&e.ID, &e.AccountID, &e.DatenklasseID, &e.MaxTage, &e.UpdatedAt); err != nil {
return nil, fmt.Errorf("store: scan loeschfrist einstellung: %w", err)
}
out = append(out, e)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("store: list loeschfrist einstellungen: %w", err)
}
return out, nil
}