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 ==="
|
||||
(cd "$m" && go vet ./... && go test ./...)
|
||||
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/
|
||||
├── README.md — diese Datei
|
||||
├── trust/
|
||||
│ └── migrations/
|
||||
│ ├── 0001_per_diem_rates.up.sql
|
||||
│ └── 0001_per_diem_rates.down.sql
|
||||
│ ├── migrations/
|
||||
│ │ ├── 0001_per_diem_rates.up.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/
|
||||
└── 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`.
|
||||
|
||||
## Konventionen
|
||||
|
|
@ -51,9 +64,16 @@ migrate -path db/trust/migrations \
|
|||
|
||||
## CI
|
||||
|
||||
Ein eigener CI-Job `db` (noch nicht committed) macht:
|
||||
1. **Lint** (`sqlfluff` oder `pg_format`).
|
||||
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.
|
||||
Der Job `db` in `.forgejo/workflows/validate.yml` macht pro PR und
|
||||
`main`-Push:
|
||||
|
||||
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