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>
140 lines
4.8 KiB
Go
140 lines
4.8 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
// BewertungAnforderung ist eine abgeleitete Anforderung mit Herleitung,
|
|
// wie sie in bewertung.anforderungen (JSONB) gespeichert wird.
|
|
type BewertungAnforderung struct {
|
|
ID string `json:"id"`
|
|
Beschreibung string `json:"beschreibung"`
|
|
Herleitung string `json:"herleitung"`
|
|
}
|
|
|
|
// BewertungAusschluss hält fest, warum ein Werkzeug für eine Bewertung
|
|
// ausgeschlossen wurde.
|
|
type BewertungAusschluss struct {
|
|
WerkzeugID string `json:"werkzeug_id"`
|
|
NichtErfuellt []string `json:"nicht_erfuellt"`
|
|
}
|
|
|
|
// Bewertung ist der berechnete Vorschlag für einen Antrag. Append-only
|
|
// — siehe Migration.
|
|
type Bewertung struct {
|
|
ID string
|
|
AntragID string
|
|
Datenklasse string
|
|
DatenklasseHerleitung string
|
|
Einstufung string
|
|
EinstufungHerleitung string
|
|
Verboten bool
|
|
Anforderungen []BewertungAnforderung
|
|
ZulaessigeWerkzeuge []string
|
|
AusgeschlosseneWerkzeuge []BewertungAusschluss
|
|
RegelwerkVersion string
|
|
KatalogVersion string
|
|
CreatedAt time.Time
|
|
}
|
|
|
|
const bewertungColumns = `id, antrag_id, datenklasse, datenklasse_herleitung, einstufung, einstufung_herleitung,
|
|
verboten, anforderungen, zulaessige_werkzeuge, ausgeschlossene_werkzeuge, regelwerk_version, katalog_version, created_at`
|
|
|
|
func scanBewertung(row interface {
|
|
Scan(dest ...any) error
|
|
}) (Bewertung, error) {
|
|
var b Bewertung
|
|
var anforderungenRaw, ausschlussRaw []byte
|
|
err := row.Scan(
|
|
&b.ID, &b.AntragID, &b.Datenklasse, &b.DatenklasseHerleitung, &b.Einstufung, &b.EinstufungHerleitung,
|
|
&b.Verboten, &anforderungenRaw, &b.ZulaessigeWerkzeuge, &ausschlussRaw, &b.RegelwerkVersion, &b.KatalogVersion, &b.CreatedAt,
|
|
)
|
|
if err != nil {
|
|
return Bewertung{}, err
|
|
}
|
|
if err := json.Unmarshal(anforderungenRaw, &b.Anforderungen); err != nil {
|
|
return Bewertung{}, fmt.Errorf("store: anforderungen unmarshal: %w", err)
|
|
}
|
|
if err := json.Unmarshal(ausschlussRaw, &b.AusgeschlosseneWerkzeuge); err != nil {
|
|
return Bewertung{}, fmt.Errorf("store: ausgeschlossene_werkzeuge unmarshal: %w", err)
|
|
}
|
|
return b, nil
|
|
}
|
|
|
|
// BewertungInput bündelt die Felder einer neuen Bewertung — bei elf
|
|
// Werten lesbarer als eine positionale Parameterliste (gleiches Muster
|
|
// wie WerkzeugInput).
|
|
type BewertungInput struct {
|
|
AntragID string
|
|
Datenklasse string
|
|
DatenklasseHerleitung string
|
|
Einstufung string
|
|
EinstufungHerleitung string
|
|
Verboten bool
|
|
Anforderungen []BewertungAnforderung
|
|
ZulaessigeWerkzeuge []string
|
|
AusgeschlosseneWerkzeuge []BewertungAusschluss
|
|
RegelwerkVersion string
|
|
KatalogVersion string
|
|
}
|
|
|
|
// CreateBewertung speichert eine berechnete Bewertung für einen Antrag.
|
|
func (s *Store) CreateBewertung(ctx context.Context, in BewertungInput) (Bewertung, error) {
|
|
if in.ZulaessigeWerkzeuge == nil {
|
|
in.ZulaessigeWerkzeuge = []string{}
|
|
}
|
|
if in.Anforderungen == nil {
|
|
in.Anforderungen = []BewertungAnforderung{}
|
|
}
|
|
if in.AusgeschlosseneWerkzeuge == nil {
|
|
in.AusgeschlosseneWerkzeuge = []BewertungAusschluss{}
|
|
}
|
|
anforderungenJSON, err := json.Marshal(in.Anforderungen)
|
|
if err != nil {
|
|
return Bewertung{}, fmt.Errorf("store: anforderungen marshal: %w", err)
|
|
}
|
|
ausschlussJSON, err := json.Marshal(in.AusgeschlosseneWerkzeuge)
|
|
if err != nil {
|
|
return Bewertung{}, fmt.Errorf("store: ausgeschlossene_werkzeuge marshal: %w", err)
|
|
}
|
|
|
|
row := s.db(ctx).QueryRow(ctx, `
|
|
INSERT INTO bewertung (
|
|
antrag_id, datenklasse, datenklasse_herleitung, einstufung, einstufung_herleitung,
|
|
verboten, anforderungen, zulaessige_werkzeuge, ausgeschlossene_werkzeuge, regelwerk_version, katalog_version
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
|
RETURNING `+bewertungColumns,
|
|
in.AntragID, in.Datenklasse, in.DatenklasseHerleitung, in.Einstufung, in.EinstufungHerleitung,
|
|
in.Verboten, anforderungenJSON, in.ZulaessigeWerkzeuge, ausschlussJSON, in.RegelwerkVersion, in.KatalogVersion,
|
|
)
|
|
b, err := scanBewertung(row)
|
|
if err != nil {
|
|
return Bewertung{}, fmt.Errorf("store: create bewertung: %w", err)
|
|
}
|
|
return b, nil
|
|
}
|
|
|
|
// GetLatestBewertungForAntrag liefert die zuletzt berechnete Bewertung
|
|
// eines Antrags. Liefert ErrNotFound, wenn noch keine Bewertung
|
|
// existiert.
|
|
func (s *Store) GetLatestBewertungForAntrag(ctx context.Context, antragID string) (Bewertung, error) {
|
|
row := s.db(ctx).QueryRow(ctx, `
|
|
SELECT `+bewertungColumns+`
|
|
FROM bewertung WHERE antrag_id = $1 ORDER BY created_at DESC LIMIT 1
|
|
`, antragID)
|
|
b, err := scanBewertung(row)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return Bewertung{}, ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return Bewertung{}, fmt.Errorf("store: get latest bewertung: %w", err)
|
|
}
|
|
return b, nil
|
|
}
|