Supply-Chain-Scan (Go) / govulncheck (push) Canceled after 0s
Schliesst die in QA-05 gefundene Luecke: der zentrale Audit-Log (AUD-01/02) existierte und war getestet, wurde aber von keinem Produktions-Handler befuellt. Additive WithAudit(...)-Methode je Store (Konvention aus lockout.Store.WithPolicy uebernommen, audit==nil bleibt gueltig, kein Verhaltensbruch fuer bestehende Aufrufer): - internal/policy.Store.Grant/Revoke -> policy.grant/policy.revoke - internal/tenant.Registry (Suspend/Reactivate/ScheduleDeletion/ CancelDeletion via transition) -> tenant.transition - internal/lockout.Store.RecordFailure/Unlock -> auth.login_failed/ auth.account_locked/auth.account_unlocked - internal/kek.Store.RotateTenantKEK/RotateMasterKey -> kek.tenant_rotated/ kek.master_rotated Neues Testpaket internal/audit/wiring_test.go: fuer jeden der vier Bereiche eine reale Aktion ausgefuehrt und per direkter audit_events-Abfrage nachgewiesen (derselbe Nachweisstil wie der QA-05-Stichprobenabgleich, der die Luecke fand). Alle bestehenden Tests der vier Pakete bleiben gruen. 51/51 Pakete gruen auf 192.168.1.131. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
97 lines
3.2 KiB
Go
97 lines
3.2 KiB
Go
package tenant
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/internal/audit"
|
|
)
|
|
|
|
// Registry kapselt den Zugriff auf die Control-Plane-Registry-Datenbank.
|
|
// Sie enthaelt ausschliesslich Tenant-Metadaten (Akzeptanzkriterium 1) —
|
|
// niemals Geschaeftsdaten eines Mandanten.
|
|
type Registry struct {
|
|
pool *pgxpool.Pool
|
|
audit *audit.Log
|
|
}
|
|
|
|
func NewRegistry(pool *pgxpool.Pool) *Registry {
|
|
return &Registry{pool: pool}
|
|
}
|
|
|
|
// WithAudit liefert eine Registry, die jeden Lebenszyklus-Uebergang
|
|
// zusaetzlich im zentralen, unveraenderlichen Audit-Log protokolliert
|
|
// (AUD-06) — rein additiv, log == nil bleibt gueltig (z.B. bestehende Tests).
|
|
func (r *Registry) WithAudit(log *audit.Log) *Registry {
|
|
return &Registry{pool: r.pool, audit: log}
|
|
}
|
|
|
|
// insertTx schreibt den Tenant-Datensatz innerhalb einer laufenden Transaktion,
|
|
// damit Provisioner.Provision DB-Anlage und Registrierung atomar behandeln kann.
|
|
func (r *Registry) insertTx(ctx context.Context, tx pgx.Tx, t Tenant) (Tenant, error) {
|
|
row := tx.QueryRow(ctx, `
|
|
INSERT INTO tenants (slug, name, db_name, db_dsn, status)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
RETURNING id, created_at
|
|
`, t.Slug, t.Name, t.DBName, t.DBDSN, t.Status)
|
|
|
|
if err := row.Scan(&t.ID, &t.CreatedAt); err != nil {
|
|
return Tenant{}, fmt.Errorf("tenant registrieren: %w", err)
|
|
}
|
|
return t, nil
|
|
}
|
|
|
|
func (r *Registry) GetBySlug(ctx context.Context, slug string) (Tenant, error) {
|
|
// previous_status/deletion_scheduled_at werden mitgelesen, damit TEN-04
|
|
// (internal/tenant/lifecycle.go) den vollstaendigen Lebenszyklus-Zustand
|
|
// ueber GetBySlug ansehen kann, statt eine eigene Abfrage zu duplizieren.
|
|
// retention_block_reason/retention_checked_at (TEN-08) aus demselben Grund
|
|
// fuer die Admin-Einsehbarkeit des Sperrgrunds (Akzeptanzkriterium 2).
|
|
row := r.pool.QueryRow(ctx, `
|
|
SELECT id, slug, name, db_name, db_dsn, status, created_at, previous_status, deletion_scheduled_at,
|
|
retention_block_reason, retention_checked_at
|
|
FROM tenants WHERE slug = $1
|
|
`, slug)
|
|
|
|
t, err := scanTenantWithLifecycle(row)
|
|
if err != nil {
|
|
return Tenant{}, fmt.Errorf("tenant laden: %w", err)
|
|
}
|
|
return t, nil
|
|
}
|
|
|
|
// Delete entfernt einen Registry-Eintrag. Wird von Provisioner.Deprovision
|
|
// als Kompensation verwendet, wenn nach erfolgreicher Registrierung ein
|
|
// nachgelagerter Schritt (z.B. Admin-Account-Anlage in TEN-02) fehlschlaegt.
|
|
func (r *Registry) Delete(ctx context.Context, id string) error {
|
|
if _, err := r.pool.Exec(ctx, `DELETE FROM tenants WHERE id = $1`, id); err != nil {
|
|
return fmt.Errorf("tenant loeschen: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Registry) List(ctx context.Context) ([]Tenant, error) {
|
|
rows, err := r.pool.Query(ctx, `
|
|
SELECT id, slug, name, db_name, db_dsn, status, created_at, previous_status, deletion_scheduled_at,
|
|
retention_block_reason, retention_checked_at
|
|
FROM tenants ORDER BY created_at
|
|
`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("tenants auflisten: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var out []Tenant
|
|
for rows.Next() {
|
|
t, err := scanTenantWithLifecycle(rows)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("tenant lesen: %w", err)
|
|
}
|
|
out = append(out, t)
|
|
}
|
|
return out, rows.Err()
|
|
}
|