ProofedMesh/services/admin-api
CTO 69b3f3012f
All checks were successful
validate / Rego (opa fmt + opa test) (push) Successful in 14s
validate / JSON Schema (ajv) (push) Successful in 20s
validate / Go (vet + test) (push) Successful in 20s
services: add transparency-log Go service (ADR-0005 POST /api/v1/log/entries)
- services/transparency-log/pkg/backend: Backend interface with in-memory
  append-log (RFC 6962 Merkle tree) + Trillian wrapper (prod stub).
- Idempotenz-Pflicht: Wiederaufruf mit identischem content_hash liefert
  200 + gleichen log_index + aktuellen inclusion_proof.
- content_hash-Verifikation serverseitig; Mismatch → 409.
- pkg/httpapi: ADR-0005-konformer Handler (201/200/400/404/409/503).
- cmd/transparency-log: minimaler HTTP-Server, memory default,
  --backend trillian als Stub (503).
- Interop-Test in services/admin-api/internal/tlog/interop_test.go
  fährt die echte transparency-log pkg gegen die admin-api
  HTTPSubmitter — Happy-Path, Idempotenz und content_hash-Mismatch
  abgedeckt.
- go test: 10 Tests transparency-log + 2 Interop-Tests admin-api, go
  vet clean in beiden Modulen.

Folge-Subtask: Trillian-Deployment + Keys (siehe ADR-0005 Open Points).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-25 18:08:53 +00:00
..
cmd/admin-api admin-api: Pocket-ID client_credentials TokenSource with OIDC discovery + cache (replaces StaticToken in prod) 2026-10-25 18:04:00 +00:00
internal services: add transparency-log Go service (ADR-0005 POST /api/v1/log/entries) 2026-10-25 18:08:53 +00:00
web/templates services: add admin-api Go service (per-diem rates CRUD, in-memory + Postgres stub store, HTML templates, 13/13 go tests pass, go vet clean) 2026-10-25 17:28:14 +00:00
admin-api services: add admin-api Go service (per-diem rates CRUD, in-memory + Postgres stub store, HTML templates, 13/13 go tests pass, go vet clean) 2026-10-25 17:28:14 +00:00
go.mod services: add transparency-log Go service (ADR-0005 POST /api/v1/log/entries) 2026-10-25 18:08:53 +00:00
README.md admin-api: Pocket-ID client_credentials TokenSource with OIDC discovery + cache (replaces StaticToken in prod) 2026-10-25 18:04:00 +00:00

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, Transparency-Log-Client und Pocket-ID-TokenSource (client_credentials mit Discovery + Cache) sind vollständig und getestet (36 Go-Tests, go vet clean). Noch Stubs: Postgres-Store (pgx) und die inbound Pocket-ID-Auth-Middleware (Admin-Session-Cookie) — 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

Admin-API hat zwei Auth-Richtungen und beide laufen über Pocket ID:

  • Inbound (Admins → Admin-API): OIDC 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. Admin-API ist human-only — Hintergrund-Dienste rufen die Admin-API nicht an.
  • Outbound (Admin-API → Signing/Transparency-Log): Pocket ID client_credentials-Grant. Admin-API ist dort als eigener OIDC-Client registriert und erhält kurzlebige Bearer-Token. Der zentrale attest.PocketIDTokenSource cached den Token und zieht ihn automatisch 30 s vor exp neu. Dev-Setups können den Flow mit -bearer=<static token> umgehen; produktiv wird -bearer leer gelassen und -oidc-issuer + POCKET_ID_CLIENT_ID + POCKET_ID_CLIENT_SECRET gesetzt.

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 (TokenSource)
│   │   ├── http_signer_test.go             — httptest, Fehlerpfade, Hash-Stabilität
│   │   ├── pocket_id_token_source.go       — Pocket-ID client_credentials + Discovery + Cache
│   │   ├── pocket_id_token_source_test.go  — httptest-OIDC-Mock, Cache + exp-30s Refresh
│   │   └── 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). Credentials nur über Env,
# nicht als Flag — damit sie nicht in Prozess-Listings landen.
export POCKET_ID_CLIENT_ID=proofedmesh-admin-api
export POCKET_ID_CLIENT_SECRET=…  # aus Pocket-ID OIDC-Client-Registrierung
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 \
  -oidc-issuer=https://auth.basicstack.de

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; umgeht den client_credentials-Flow
-oidc-issuer https://auth.basicstack.de Pocket-ID-Base-URL — Discovery für client_credentials-Flow + Issuer-Claim für performed_by.iss
-pocket-id-scope (leer) Optionaler scope im client_credentials-Request
-pocket-id-audience (leer) Optionale audience im client_credentials-Request

Zusätzliche Env-Variablen (werden nur im client_credentials-Flow gelesen, also wenn -bearer leer ist und -oidc-issuer gesetzt ist):

Env Beschreibung
POCKET_ID_CLIENT_ID OIDC-Client-ID aus der admin-api-Registrierung in Pocket ID
POCKET_ID_CLIENT_SECRET OIDC-Client-Secret aus der admin-api-Registrierung in Pocket ID

Fehlt eins der beiden, aber -oidc-issuer ist gesetzt und -bearer ist leer, bricht admin-api direkt beim Start ab — kein stilles Weiterlaufen mit nicht authentifizierten Signatur-Requests.

Pocket-ID-Client-Registrierung für admin-api

Für den Outbound-Flow (Admin-API → Signing/Transparency-Log) muss admin-api in Pocket ID als eigener OIDC-Client angelegt werden.

Admin-UI (https://auth.basicstack.de/settings/oidc-clients):

  • Name: proofedmesh-admin-api
  • Grant Type: client_credentials
  • Redirect URIs: leer lassen (kein Browser-Flow)
  • PKCE: deaktivieren (Server-zu-Server-Flow, kein User-Agent)
  • Scopes: default; falls Signing-Service später eine eigene Scope erzwingt, hier zusätzlich eintragen und per -pocket-id-scope anfordern
  • Allowed user groups: leer (kein User-bezogener Flow)

Alternativ via API, siehe skills/pocket-id-api und POST /api/oidc/clients:

curl -X POST "https://auth.basicstack.de/api/oidc/clients" \
  -H "X-API-Key: $POCKET_ID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "proofedmesh-admin-api",
    "callbackUrls": [],
    "pkceEnabled": false,
    "isPublic": false,
    "credentialsConfidentialClient": true
  }'

Die clientId und das generierte clientSecret landen danach als Secrets in der Deployment-Pipeline und werden dem admin-api-Container als POCKET_ID_CLIENT_ID / POCKET_ID_CLIENT_SECRET injiziert. Die Felder sind NICHT in -flags konfigurierbar, damit sie nicht in Prozess-Listings landen.

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.