ProofedMesh/services/admin-api/README.md
CTO Agent ca62108f70
All checks were successful
validate / Rego (opa fmt + opa test) (push) Successful in 13s
validate / JSON Schema (ajv) (push) Successful in 20s
validate / Go (vet + test) (push) Successful in 19s
admin-api: wire Signer+TLog into Create/Retire handlers; on tlog outage row stays attestation-pending (26/26 tests pass, go vet clean)
2026-10-25 17:51:53 +00:00

7.7 KiB
Raw Blame History

admin-api

Admin-only HTTP service for ProofedMesh. Currently covers one concern: Verwaltung der Per-Diem-Pauschalen (per_diem_rates), die vom agent-per-diem-calculator deterministisch angewandt werden (siehe docs/architecture/v1.1.md §6.2 und ADR-0003).

Status: in-progress. HTTP-Handler, HTML-Templates, Modell-Typen, Store-Interface, Signing-Service-Client und Transparency-Log-Client sind vollständig und getestet (26 Go-Tests, go vet clean). Noch Stubs: Postgres-Store (pgx) und Pocket-ID-Auth-Middleware — werden in Phase 2 nachgezogen. Prod-Deployment folgt nach Phase-1-Trust-Core.

Scope

  • Admin-Oberfläche /admin/per-diem-rates — Liste + Neuanlage + „Retire" (valid_to setzen) über HTML + HTMX-ähnlichen simple-POST-Flow.
  • JSON-API hinter denselben Pfaden für Automatisierung (Accept: application/json).
  • Jede Create/Retire-Operation erzeugt eine Rate Change Attestation über den Signing Service — predicateType https://basicstack.de/attestations/rate_change/v1. Rückgabe-Hash wird in per_diem_rates.rate_change_attestation_ref sowie in per_diem_rate_changes persistiert.
  • Keine Delete-Operation. Historie ist unveränderlich (Schema-Trigger erzwingt das).

Auth

  • OIDC via Pocket ID (Authorization-Code-Flow, Session-Cookie).
  • Autorisierung: Gruppenmitgliedschaft proofedmesh-admin wird aus dem ID-Token gelesen und ist Pflicht für alle Endpunkte außer Login.
  • Hintergrund-Dienste rufen die API nicht — Admin-API ist Human-only. Deshalb kein client_credentials-Pfad.

HTTP-Endpunkte (geplant)

Methode Pfad Beschreibung
GET /admin/per-diem-rates/ Liste aller Rates (gefiltert nach Country, aktiv/retired)
GET /admin/per-diem-rates/{rate_id} Detailansicht einer Rate
POST /admin/per-diem-rates/ Neue Rate anlegen (JSON oder Form)
POST /admin/per-diem-rates/{rate_id}/retire valid_to setzen (Rate ablaufen lassen)

Content-Negotiation per Accept-Header: text/html → Templates, application/json → JSON.

Layout

services/admin-api/
├── README.md                       — diese Datei
├── go.mod
├── cmd/admin-api/main.go           — HTTP-Entry (bind + router + graceful shutdown)
├── internal/
│   ├── model/rate.go               — Rate struct, validation
│   ├── store/
│   │   ├── store.go                — RateStore interface
│   │   ├── memory.go               — in-memory impl (für Tests + Dev)
│   │   ├── memory_test.go
│   │   └── postgres.go             — pgx impl, derzeit Stub
│   ├── httpapi/
│   │   ├── rates.go                        — Handler (JSON + HTML, Signer-Orchestrierung)
│   │   ├── rates_test.go                   — Handler-Tests gegen in-memory store
│   │   └── attest_integration_test.go      — HTTP+Signer+TLog end-to-end
│   ├── attest/
│   │   ├── ratechange.go                   — Statement-Bau, Orchestrator SignAndLog
│   │   ├── hash.go                         — stabile JSON-Serialisierung + SHA-256
│   │   ├── http_signer.go                  — POST /api/v1/sign/dsse Client
│   │   ├── http_signer_test.go             — httptest, Fehlerpfade, Hash-Stabilität
│   │   └── orchestrator_test.go            — SignAndLog mit Fake-Signer/-Submitter
│   └── tlog/
│       ├── submitter.go                    — POST /api/v1/log/entries Client
│       └── submitter_test.go               — httptest, Idempotenz, Fehlerpfade
└── web/
    └── templates/per_diem_rates_{list,detail}.html

Lokal bauen

cd services/admin-api
go vet ./... && go test ./...
# Dev-Modus (keine externen Dienste nötig):
go run ./cmd/admin-api -store=memory -listen=:8080
# Dev-Modus mit echter Attestation-Pipeline (lokale Mock-Server):
go run ./cmd/admin-api -store=memory \
  -signer-url=http://localhost:9001 -tlog-url=http://localhost:9002 \
  -bearer=dev-token
# Prod-Modus (sobald postgres.go fertig):
go run ./cmd/admin-api -store=postgres \
  -db="postgres://trust:…@trust-postgres:5432/trust?sslmode=verify-full" \
  -signer-url=https://trust.basicstack.de -tlog-url=https://trust.basicstack.de \
  -signer-key=trust-api/rate-change-signer-2026-q4

Attestations-Fluss (implementiert)

Jede erfolgreiche Create/Retire-Operation ruft intern attest.Orchestrator.SignAndLog:

  1. attest.buildStatement(event) baut ein in-toto v1 Statement mit stabilem subject.digest (SHA-256 der kern-identifizierenden Rate-Felder, excludes valid_to und rate_change_attestation_ref).
  2. attest.HTTPSigner.SignDSSE(ctx, keyRef, stmt) ruft POST /api/v1/sign/dsse (ADR-0005) — gibt DSSE-Envelope + content_hash zurück.
  3. tlog.HTTPSubmitter.Submit(ctx, envelope, content_hash) ruft POST /api/v1/log/entries — gibt log_index + inclusion_proof zurück. Idempotent: Wiederaufruf mit demselben content_hash liefert dieselbe Antwort.
  4. store.AttachAttestation(rate_id, content_hash) schreibt rate_change_attestation_ref in die DB.

Fehlerpfad: Scheitert Schritt 2 oder 3, bleibt die Row bestehen, aber ohne rate_change_attestation_ref. Die Row ist dann "attestation pending" — ein künftiger Reconciliation-Worker (eigenes Service, später) wiederholt Schritte 2–4 für solche Rows. Der Handler loggt den Fehler, antwortet dem Admin aber mit 201 Created, weil der eigentliche DB-Insert erfolgreich war (die Historie ist mindestens lokal konsistent).

Konfigurations-Flags

Flag Default Beschreibung
-listen :8080 HTTP-Bind
-store memory memory oder postgres
-signer-url (leer) Signing-Service-Base-URL — leer → Attestation deaktiviert
-tlog-url (leer) Transparency-Log-Base-URL — leer → Attestation deaktiviert
-signer-key trust-api/rate-change-signer-2026-q4 Signing-Key-Referenz (OpenBao Transit)
-bearer (leer) Statischer Pocket-ID-Token für Dev (Prod: client_credentials-Flow, noch nicht gewired)
-oidc-issuer https://auth.basicstack.de Issuer-Claim für performed_by.iss im Attestations-Predicate

Rate Change Attestation (Entwurf)

{
  "_type": "https://in-toto.io/Statement/v1",
  "subject": [
    { "name": "per_diem_rate/PDR-42", "digest": { "sha256": "<content hash>" } }
  ],
  "predicateType": "https://basicstack.de/attestations/rate_change/v1",
  "predicate": {
    "change_kind": "create",
    "rate_id": "PDR-42",
    "country": "DE",
    "city": null,
    "currency": "EUR",
    "full_day": "28.00",
    "partial_day_min8h": "14.00",
    "arrival_day": "14.00",
    "departure_day": "14.00",
    "valid_from": "2026-01-01",
    "valid_to": null,
    "source_regulation": "BMF-Schreiben …",
    "performed_by": {
      "iss": "https://auth.basicstack.de",
      "sub": "user-admin-42",
      "session_id": "SES-admin-…"
    },
    "performed_at": "2026-10-25T18:30:00Z"
  }
}

Signiert vom Signing Service, Attestations-Hash wandert zurück in die Tabellen per_diem_rates.rate_change_attestation_ref und per_diem_rate_changes.rate_change_attestation_ref.