db+ci: Agent Registry schema (agents + agent_keys), Phase-1 seed, migrate up/down CI (DEV-607)
Some checks failed
build-images / signing-service image (push) Failing after 14s
validate / JSON Schema (ajv) (push) Successful in 19s
validate / Rego (opa fmt + opa test) (push) Successful in 14s
validate / DB (migrate up/down dry-run) (push) Failing after 18s
validate / Go (vet + test) (push) Successful in 34s
Some checks failed
build-images / signing-service image (push) Failing after 14s
validate / JSON Schema (ajv) (push) Successful in 19s
validate / Rego (opa fmt + opa test) (push) Successful in 14s
validate / DB (migrate up/down dry-run) (push) Failing after 18s
validate / Go (vet + test) (push) Successful in 34s
- 0002_agents / 0003_agent_keys migrations per architecture v1.1 §4.4 (incl. (k8s_namespace, k8s_service_account) uniqueness for the signing service's dual identity check and an agent_id, valid_from index for the verification lookup path) - idempotent seed db/trust/seeds/0001_phase1_agents.sql with the three Phase-1 identities: agent-approval-service, agent-testagent-transform, agent-dummy-sink (namespace agent-trust) - new db CI job in validate.yml: runs postgres:16-alpine, migrate up -> down 2 -> up, applies the seed twice, verifies three agents Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
951f22f80e
commit
51733d8b36
7 changed files with 258 additions and 8 deletions
|
|
@ -154,3 +154,117 @@ jobs:
|
||||||
echo "=== $m ==="
|
echo "=== $m ==="
|
||||||
(cd "$m" && go vet ./... && go test ./...)
|
(cd "$m" && go vet ./... && go test ./...)
|
||||||
done < /tmp/go_modules.txt
|
done < /tmp/go_modules.txt
|
||||||
|
|
||||||
|
db:
|
||||||
|
name: DB (migrate up/down dry-run)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: debian:trixie-slim
|
||||||
|
options: --user 0:0
|
||||||
|
env:
|
||||||
|
# Host-networked job container → localhost = host; use a non-default
|
||||||
|
# port so parallel jobs do not collide on 5432 if any happen to share
|
||||||
|
# a runner host.
|
||||||
|
PGHOST: 127.0.0.1
|
||||||
|
PGPORT: "55432"
|
||||||
|
PGUSER: postgres
|
||||||
|
PGPASSWORD: postgres
|
||||||
|
PGDATABASE: trust
|
||||||
|
DB_URL: postgres://postgres:postgres@127.0.0.1:55432/trust?sslmode=disable
|
||||||
|
PG_IMAGE: postgres:16-alpine
|
||||||
|
PG_CONTAINER: pm-ci-pg-${{ github.run_id }}-${{ github.run_attempt }}
|
||||||
|
steps:
|
||||||
|
- name: Install git + curl + docker CLI + psql
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
git curl ca-certificates docker.io postgresql-client
|
||||||
|
git --version
|
||||||
|
docker --version
|
||||||
|
psql --version
|
||||||
|
|
||||||
|
- name: Clone repository (sha256-aware)
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
git clone --depth=1 --branch="${GITHUB_REF_NAME}" \
|
||||||
|
"https://x-access-token:${GITHUB_TOKEN}@forgejo.basicstack.de/${GITHUB_REPOSITORY}.git" \
|
||||||
|
"${GITHUB_WORKSPACE}"
|
||||||
|
cd "${GITHUB_WORKSPACE}"
|
||||||
|
git log -1 --oneline
|
||||||
|
|
||||||
|
- name: Install golang-migrate CLI
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
curl -sSL -o /tmp/migrate.tgz \
|
||||||
|
https://github.com/golang-migrate/migrate/releases/download/v4.17.1/migrate.linux-amd64.tar.gz
|
||||||
|
tar -xzf /tmp/migrate.tgz -C /usr/local/bin migrate
|
||||||
|
migrate -version
|
||||||
|
|
||||||
|
- name: Start Postgres (via docker)
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
docker run -d --name "${PG_CONTAINER}" \
|
||||||
|
-e POSTGRES_PASSWORD="${PGPASSWORD}" \
|
||||||
|
-e POSTGRES_USER="${PGUSER}" \
|
||||||
|
-e POSTGRES_DB="${PGDATABASE}" \
|
||||||
|
-p "${PGPORT}:5432" \
|
||||||
|
"${PG_IMAGE}"
|
||||||
|
# Wait up to 60s for pg to be ready.
|
||||||
|
for i in $(seq 1 60); do
|
||||||
|
if pg_isready -h "${PGHOST}" -p "${PGPORT}" -U "${PGUSER}" -d "${PGDATABASE}" >/dev/null 2>&1; then
|
||||||
|
echo "postgres ready after ${i}s"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
echo "postgres never became ready" >&2
|
||||||
|
docker logs "${PG_CONTAINER}" || true
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: migrate up (full) + verify tables
|
||||||
|
working-directory: ${{ github.workspace }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
migrate -path db/trust/migrations -database "${DB_URL}" up
|
||||||
|
psql "${DB_URL}" -c '\dt' | tee /tmp/dt-after-up.txt
|
||||||
|
grep -q ' per_diem_rates ' /tmp/dt-after-up.txt
|
||||||
|
grep -q ' agents ' /tmp/dt-after-up.txt
|
||||||
|
grep -q ' agent_keys ' /tmp/dt-after-up.txt
|
||||||
|
|
||||||
|
- name: migrate down 2 + verify only agent tables gone
|
||||||
|
working-directory: ${{ github.workspace }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
migrate -path db/trust/migrations -database "${DB_URL}" down 2
|
||||||
|
psql "${DB_URL}" -c '\dt' | tee /tmp/dt-after-down.txt
|
||||||
|
grep -q ' per_diem_rates ' /tmp/dt-after-down.txt
|
||||||
|
if grep -q ' agents ' /tmp/dt-after-down.txt; then
|
||||||
|
echo "FAIL: agents still present after migrate down 2" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if grep -q ' agent_keys ' /tmp/dt-after-down.txt; then
|
||||||
|
echo "FAIL: agent_keys still present after migrate down 2" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: migrate up (re-apply) + apply seed + verify three agents
|
||||||
|
working-directory: ${{ github.workspace }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
migrate -path db/trust/migrations -database "${DB_URL}" up
|
||||||
|
psql "${DB_URL}" -v ON_ERROR_STOP=1 -f db/trust/seeds/0001_phase1_agents.sql
|
||||||
|
# Idempotency: re-run must not error and not duplicate rows.
|
||||||
|
psql "${DB_URL}" -v ON_ERROR_STOP=1 -f db/trust/seeds/0001_phase1_agents.sql
|
||||||
|
count=$(psql "${DB_URL}" -At -c \
|
||||||
|
"SELECT count(*) FROM agents WHERE agent_id IN ('agent-approval-service','agent-testagent-transform','agent-dummy-sink')")
|
||||||
|
echo "seeded agents = ${count}"
|
||||||
|
[ "${count}" = "3" ]
|
||||||
|
|
||||||
|
- name: Postgres logs (always)
|
||||||
|
if: always()
|
||||||
|
run: docker logs "${PG_CONTAINER}" || true
|
||||||
|
|
||||||
|
- name: Stop Postgres (always)
|
||||||
|
if: always()
|
||||||
|
run: docker rm -f "${PG_CONTAINER}" || true
|
||||||
|
|
|
||||||
36
db/README.md
36
db/README.md
|
|
@ -8,13 +8,26 @@ Alle Postgres-Schemas von ProofedMesh werden mit **[golang-migrate](https://gith
|
||||||
db/
|
db/
|
||||||
├── README.md — diese Datei
|
├── README.md — diese Datei
|
||||||
├── trust/
|
├── trust/
|
||||||
│ └── migrations/
|
│ ├── migrations/
|
||||||
│ ├── 0001_per_diem_rates.up.sql
|
│ │ ├── 0001_per_diem_rates.up.sql
|
||||||
│ └── 0001_per_diem_rates.down.sql
|
│ │ ├── 0001_per_diem_rates.down.sql
|
||||||
|
│ │ ├── 0002_agents.up.sql
|
||||||
|
│ │ ├── 0002_agents.down.sql
|
||||||
|
│ │ ├── 0003_agent_keys.up.sql
|
||||||
|
│ │ └── 0003_agent_keys.down.sql
|
||||||
|
│ └── seeds/
|
||||||
|
│ └── 0001_phase1_agents.sql
|
||||||
└── expense/
|
└── expense/
|
||||||
└── migrations/ — kommt im Phase-4-Rollout der DB-App
|
└── migrations/ — kommt im Phase-4-Rollout der DB-App
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Seeds werden **nicht** von `migrate` versioniert — sie sind idempotentes
|
||||||
|
Bootstrap-SQL für Phase-1-Fixturen (z.B. die drei Agent-Identitäten) und
|
||||||
|
werden per `psql -f` oder äquivalentem Kubernetes-Job eingespielt. Jeder
|
||||||
|
Seed MUSS `ON CONFLICT … DO NOTHING` / `IF NOT EXISTS` verwenden, damit
|
||||||
|
mehrfaches Einspielen nach einem `migrate down/up`-Zyklus keine Duplikate
|
||||||
|
produziert.
|
||||||
|
|
||||||
Jede logische Datenbank bekommt ein eigenes `migrations/`-Verzeichnis. Die Migrationstabelle (`schema_migrations`) wird je Datenbank separat gepflegt. Pro Postgres-Instanz können mehrere logische Schemas liegen — die Migrationen schreiben SQL mit `CREATE SCHEMA IF NOT EXISTS` und `SET search_path`.
|
Jede logische Datenbank bekommt ein eigenes `migrations/`-Verzeichnis. Die Migrationstabelle (`schema_migrations`) wird je Datenbank separat gepflegt. Pro Postgres-Instanz können mehrere logische Schemas liegen — die Migrationen schreiben SQL mit `CREATE SCHEMA IF NOT EXISTS` und `SET search_path`.
|
||||||
|
|
||||||
## Konventionen
|
## Konventionen
|
||||||
|
|
@ -51,9 +64,16 @@ migrate -path db/trust/migrations \
|
||||||
|
|
||||||
## CI
|
## CI
|
||||||
|
|
||||||
Ein eigener CI-Job `db` (noch nicht committed) macht:
|
Der Job `db` in `.forgejo/workflows/validate.yml` macht pro PR und
|
||||||
1. **Lint** (`sqlfluff` oder `pg_format`).
|
`main`-Push:
|
||||||
2. **Dry-run** auf einer wegwerfbaren Postgres-Instanz: `up` → `down` → `up`, muss grün sein.
|
|
||||||
3. **Keine History-Rewrites:** vorhandene Dateien dürfen in einem PR nicht geändert werden.
|
|
||||||
|
|
||||||
Kommt in einem separaten Heartbeat.
|
1. Startet `postgres:16-alpine` als Container.
|
||||||
|
2. `migrate up` → prüft, dass `per_diem_rates`, `agents` und `agent_keys`
|
||||||
|
angelegt sind.
|
||||||
|
3. `migrate down 2` → prüft, dass `agents` und `agent_keys` wieder
|
||||||
|
entfernt sind, `per_diem_rates` aber bleibt (Trenn-Test).
|
||||||
|
4. `migrate up` erneut und spielt `db/trust/seeds/0001_phase1_agents.sql`
|
||||||
|
zweimal ein (Idempotenz-Test, Erwartung: genau 3 Seed-Zeilen).
|
||||||
|
|
||||||
|
Lint (`sqlfluff` / `pg_format`) ist noch nicht aktiviert — kommt in
|
||||||
|
einem separaten Heartbeat.
|
||||||
|
|
|
||||||
6
db/trust/migrations/0002_agents.down.sql
Normal file
6
db/trust/migrations/0002_agents.down.sql
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
-- Rollback for 0002_agents.
|
||||||
|
-- DESTRUKTIV: entfernt die Agent-Identitäten vollständig. Nur in Prod
|
||||||
|
-- anwenden, wenn 0003_agent_keys vorher heruntergefahren wurde (FK ins
|
||||||
|
-- agents(agent_id)) UND ein frischer Dump des trust-Postgres vorliegt.
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS agents;
|
||||||
33
db/trust/migrations/0002_agents.up.sql
Normal file
33
db/trust/migrations/0002_agents.up.sql
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
-- Migration: 0002_agents
|
||||||
|
-- Agent Registry (Identitäten) gemäß docs/architecture/v1.1.md §4.4.
|
||||||
|
--
|
||||||
|
-- Phase 1 nutzt diese Tabelle rein lesend (SELECT) aus Signing Service und
|
||||||
|
-- Verification Service, um Pocket-ID-Client-IDs und K8s-ServiceAccount-Paare
|
||||||
|
-- auf stabile agent_ids zu mappen. Eine HTTP-API-Schale folgt in Phase 2.
|
||||||
|
--
|
||||||
|
-- Design-Entscheidungen:
|
||||||
|
-- * agent_id ist ein stabiler, lesbarer Slug (`agent-<rolle>`), kein UUID.
|
||||||
|
-- Er taucht in Attestations (`actor`, `issuer`), DSSE-key_ids,
|
||||||
|
-- Policy-Dateien und Audit-Logs auf — Lesbarkeit zählt.
|
||||||
|
-- * (k8s_namespace, k8s_service_account) ist zusätzlich UNIQUE: die
|
||||||
|
-- doppelte Identitätsprüfung im Signing Service (Pocket-ID + K8s
|
||||||
|
-- TokenReview) MUSS auf genau einen Agenten mappen.
|
||||||
|
-- * allowed_actions ist NOT NULL, darf aber eine leere Liste sein; die
|
||||||
|
-- effektive Autorisierung macht OPA im Signing / Verification Service.
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS agents (
|
||||||
|
agent_id TEXT PRIMARY KEY
|
||||||
|
CHECK (agent_id ~ '^[a-z0-9][a-z0-9-]{0,126}[a-z0-9]$'),
|
||||||
|
pocket_id_client TEXT UNIQUE NOT NULL
|
||||||
|
CHECK (length(pocket_id_client) BETWEEN 1 AND 256),
|
||||||
|
k8s_namespace TEXT NOT NULL
|
||||||
|
CHECK (k8s_namespace ~ '^[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$'),
|
||||||
|
k8s_service_account TEXT NOT NULL
|
||||||
|
CHECK (k8s_service_account ~ '^[a-z0-9][a-z0-9.-]{0,251}[a-z0-9]$'),
|
||||||
|
display_name TEXT NOT NULL
|
||||||
|
CHECK (length(display_name) BETWEEN 1 AND 200),
|
||||||
|
allowed_actions TEXT[] NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
CONSTRAINT agents_k8s_identity_unique
|
||||||
|
UNIQUE (k8s_namespace, k8s_service_account)
|
||||||
|
);
|
||||||
7
db/trust/migrations/0003_agent_keys.down.sql
Normal file
7
db/trust/migrations/0003_agent_keys.down.sql
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- Rollback for 0003_agent_keys.
|
||||||
|
-- DESTRUKTIV: entfernt die gesamte Schlüsselhistorie aller Agents. Die
|
||||||
|
-- Signaturen in Trillian bleiben gültig, aber Verification kann sie
|
||||||
|
-- lokal nicht mehr auflösen. Vor Anwendung IMMER einen Dump des
|
||||||
|
-- trust-Postgres anfertigen.
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS agent_keys;
|
||||||
43
db/trust/migrations/0003_agent_keys.up.sql
Normal file
43
db/trust/migrations/0003_agent_keys.up.sql
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
-- Migration: 0003_agent_keys
|
||||||
|
-- Signatur-Schlüsselhistorie je Agent gemäß docs/architecture/v1.1.md §4.4.
|
||||||
|
--
|
||||||
|
-- Design-Entscheidungen (siehe §4.4, TUF-Pattern):
|
||||||
|
-- * public_key_pem wird als PEM gespeichert, Format (Ed25519/ECDSA/RSA)
|
||||||
|
-- wird NICHT in der DB erzwungen — die signing-/verification-Services
|
||||||
|
-- parsen und entscheiden. Rotation Default alle 90 Tage, Retention
|
||||||
|
-- 1 Jahr → max. ~5 nicht-archivierte Schlüssel pro Agent.
|
||||||
|
-- * valid_from / valid_to spannen den Autoritätszeitraum. Verifikation
|
||||||
|
-- alter Attestations wählt den Schlüssel, bei dem issued_at im
|
||||||
|
-- [valid_from, valid_to] liegt. valid_to ist NOT NULL (bei Ausstellung
|
||||||
|
-- wird ein geplantes Ablaufdatum gesetzt, auch wenn später rotiert
|
||||||
|
-- wird).
|
||||||
|
-- * revoked_at ist eine ausserordentliche Verkürzung (Kompromittierung).
|
||||||
|
-- Verification muss revoked_at <= issued_at ausschliessen.
|
||||||
|
-- * archived_at markiert Schlüssel, die weder aktuell noch für Retention
|
||||||
|
-- verifiziert werden müssen — nur noch Audit-Zeuge.
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS agent_keys (
|
||||||
|
key_id TEXT PRIMARY KEY
|
||||||
|
CHECK (length(key_id) BETWEEN 1 AND 256),
|
||||||
|
agent_id TEXT NOT NULL REFERENCES agents(agent_id),
|
||||||
|
public_key_pem TEXT NOT NULL
|
||||||
|
CHECK (position('-----BEGIN ' IN public_key_pem) = 1),
|
||||||
|
valid_from TIMESTAMPTZ NOT NULL,
|
||||||
|
valid_to TIMESTAMPTZ NOT NULL,
|
||||||
|
revoked_at TIMESTAMPTZ,
|
||||||
|
revocation_reason TEXT CHECK (revocation_reason IS NULL
|
||||||
|
OR length(revocation_reason) BETWEEN 1 AND 500),
|
||||||
|
archived_at TIMESTAMPTZ,
|
||||||
|
CONSTRAINT agent_keys_valid_range CHECK (valid_to > valid_from),
|
||||||
|
CONSTRAINT agent_keys_revocation_consistent
|
||||||
|
CHECK ((revoked_at IS NULL) = (revocation_reason IS NULL))
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Aktive-Schlüssel-Scan je Agent (TUF-Pattern, §4.4).
|
||||||
|
CREATE INDEX idx_agent_keys_lookup
|
||||||
|
ON agent_keys (agent_id, archived_at)
|
||||||
|
WHERE archived_at IS NULL;
|
||||||
|
|
||||||
|
-- Verification-Pfad: finde Schlüssel, der zu einem gegebenen issued_at galt.
|
||||||
|
CREATE INDEX idx_agent_keys_agent_valid_from
|
||||||
|
ON agent_keys (agent_id, valid_from);
|
||||||
27
db/trust/seeds/0001_phase1_agents.sql
Normal file
27
db/trust/seeds/0001_phase1_agents.sql
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
-- Seed: Phase-1-Agent-Identitäten (DEV-607).
|
||||||
|
--
|
||||||
|
-- Diese drei Agents sind der minimale Satz für den Phase-1-End-to-End-Smoke:
|
||||||
|
-- agent-approval-service — signiert Approval-Attestations.
|
||||||
|
-- agent-testagent-transform — signiert Transform-Attestations.
|
||||||
|
-- agent-dummy-sink — Mock-Senke (`sink:dummy-sink`), signiert
|
||||||
|
-- handover-Attestations.
|
||||||
|
--
|
||||||
|
-- Namespace `agent-trust` ist mit deploy/signing-service/ und deploy/trillian/
|
||||||
|
-- konsistent. pocket_id_client ist der stabile OIDC-client_id, den Pocket ID
|
||||||
|
-- für diesen Agent ausstellt — in Phase 1 identisch zum agent_id-Slug,
|
||||||
|
-- Subtask 7 (Agent-Registry-HTTP-API / Pocket-ID-Provisionierung) kann
|
||||||
|
-- diesen Wert später per UPDATE angleichen, wenn Pocket ID eine andere
|
||||||
|
-- client_id erzwingt.
|
||||||
|
--
|
||||||
|
-- Dieser Seed berührt agent_keys NICHT. Die Signatur-Schlüssel landen über
|
||||||
|
-- die Rotations-Pipeline (`services/signing-service` Admin-Pfad, Phase 2).
|
||||||
|
-- Idempotent: ON CONFLICT (agent_id) DO NOTHING, damit mehrfaches Einspielen
|
||||||
|
-- und ein `migrate down 2 && migrate up` den Seed nicht duplizieren.
|
||||||
|
|
||||||
|
INSERT INTO agents
|
||||||
|
(agent_id, pocket_id_client, k8s_namespace, k8s_service_account, display_name, allowed_actions)
|
||||||
|
VALUES
|
||||||
|
('agent-approval-service', 'agent-approval-service', 'agent-trust', 'approval-service', 'Approval Service', ARRAY['expense.submit']),
|
||||||
|
('agent-testagent-transform', 'agent-testagent-transform', 'agent-trust', 'testagent-transform', 'Test Agent (Transform)', ARRAY['expense.normalize', 'expense.map_to_sink']),
|
||||||
|
('agent-dummy-sink', 'agent-dummy-sink', 'agent-trust', 'dummy-sink', 'Dummy Sink (sink:dummy-sink)', ARRAY['expense.post'])
|
||||||
|
ON CONFLICT (agent_id) DO NOTHING;
|
||||||
Loading…
Add table
Reference in a new issue