Compare commits

..
Author SHA1 Message Date
patrickandClaude Sonnet 5.5 f708d0ea87 ai-app: Deployment-Anbindung (Port 8900, Migrationswerkzeug, systemd-Template)
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (pull_request) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Successful in 3s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Failing after 15s
CI / Web (pdf-viewer) (push) Failing after 34s
Supply-Chain-Scan (Go) / govulncheck (push) Failing after 17s
CI / Core-Modul (Build/Vet/Test) (push) Successful in 42s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Successful in 2s
CI / Web (dms-browser) (push) Failing after 53s
CI / Web (shl) (push) Failing after 7s
AI-Pflichttest-Gate / pflichttest-gate (pull_request) Successful in 1m57s
CI / Core-Modul (Build/Vet/Test) (pull_request) Successful in 41s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (pull_request) Successful in 3s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (pull_request) Successful in 2s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (pull_request) Failing after 15s
CI / Web (dms-browser) (pull_request) Failing after 59s
CI / Web (pdf-viewer) (pull_request) Failing after 51s
CI / Web (shl) (pull_request) Failing after 9s
Core-Schnittstellen-Vertragstests / contract-tests (pull_request) Successful in 6m41s
Supply-Chain-Scan (Go) / govulncheck (pull_request) Failing after 42s
Supply-Chain-Scan (npm) / npm-audit (pull_request) Failing after 1m28s
Core-Pflichttest-Gate / pflichttest-gate (pull_request) Successful in 2m18s
RBAC-Rollenhierarchie-Pflichtgate / rbac-policy-invariant (pull_request) Successful in 2m12s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Failing after 3s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (pull_request) Failing after 2s
DB-Integrationstests / db-tests (pull_request) Canceled after 0s
Standard-Port in den fuer ai reservierten Bereich verschoben (9600 belegt
workflow-app). Fortschrittstabelle ai_schema_migrations statt
connect_schema_migrations.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 23:07:51 +01:00
patrick 998aaf964c Merge pull request 'connect: DATEV-Export in connect-app registrieren' (#429) from feat/connect-datev-registrieren into main
CI / Web (pdf-viewer) (push) Failing after 40s
CI / Web (shl) (push) Failing after 8s
Supply-Chain-Scan (Go) / govulncheck (push) Failing after 43s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Failing after 29s
CI / Core-Modul (Build/Vet/Test) (push) Successful in 38s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Successful in 3s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Successful in 3s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Failing after 14s
CI / Web (dms-browser) (push) Failing after 54s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Failing after 2s
2026-10-30 00:24:20 +01:00
patrickandClaude Sonnet 5.5 976878136f Merge main in feat/connect-datev-registrieren
CI / Web (shl) (pull_request) Failing after 8s
Supply-Chain-Scan (Go) / govulncheck (push) Failing after 46s
RBAC-Rollenhierarchie-Pflichtgate / rbac-policy-invariant (pull_request) Successful in 19m25s
Core-Schnittstellen-Vertragstests / contract-tests (pull_request) Successful in 18m53s
Supply-Chain-Scan (Go) / govulncheck (pull_request) Failing after 44s
Supply-Chain-Scan (npm) / npm-audit (pull_request) Failing after 1m20s
Core-Pflichttest-Gate / pflichttest-gate (pull_request) Successful in 19m26s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Failing after 2s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (pull_request) Failing after 2s
DB-Integrationstests / db-tests (pull_request) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Successful in 3s
CI / Core-Modul (Build/Vet/Test) (push) Successful in 44s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Successful in 4s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Failing after 14s
CI / Web (dms-browser) (push) Failing after 1m1s
CI / Web (pdf-viewer) (push) Failing after 37s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (pull_request) Canceled after 0s
CI / Web (shl) (push) Failing after 9s
CI / Core-Modul (Build/Vet/Test) (pull_request) Successful in 44s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (pull_request) Successful in 2s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (pull_request) Successful in 3s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (pull_request) Failing after 14s
CI / Web (dms-browser) (pull_request) Failing after 55s
CI / Web (pdf-viewer) (pull_request) Failing after 41s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 00:24:14 +01:00
patrick dc8f6476ab Merge pull request 'connect/statusapi: Rolle aus dem Token an die Policy' (#428) from fix/statusapi-rolle into main
CI / Core-Modul (Build/Vet/Test) (push) Successful in 49s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Successful in 4s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Failing after 2s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Successful in 4s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Failing after 14s
CI / Web (dms-browser) (push) Failing after 1m1s
CI / Web (pdf-viewer) (push) Failing after 50s
CI / Web (shl) (push) Failing after 9s
Supply-Chain-Scan (Go) / govulncheck (push) Failing after 50s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
2026-10-30 00:23:50 +01:00
patrick bc95515d98 Merge pull request 'workflow, connect: bearerauth aus common' (#427) from refactor/bearerauth-common into main
CI / Web (pdf-viewer) (push) Failing after 35s
CI / Web (shl) (push) Failing after 7s
CI / Core-Modul (Build/Vet/Test) (push) Successful in 42s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Successful in 3s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Successful in 3s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Failing after 15s
CI / Web (dms-browser) (push) Failing after 52s
common Versions-Tag / tag (push) Successful in 19m12s
Supply-Chain-Scan (Go) / govulncheck (push) Failing after 42s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Failing after 2s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
2026-10-30 00:23:48 +01:00
patrickandClaude Sonnet 5.5 753af35888 connect: DATEV-Export in connect-app registriert, mit Rechtepruefung
Supply-Chain-Scan (Go) / govulncheck (pull_request) Failing after 42s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Failing after 2s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (pull_request) Failing after 2s
DB-Integrationstests / db-tests (pull_request) Canceled after 0s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (pull_request) Canceled after 0s
CI / Web (pdf-viewer) (push) Failing after 36s
CI / Web (shl) (push) Failing after 7s
Supply-Chain-Scan (Go) / govulncheck (push) Failing after 43s
CI / Core-Modul (Build/Vet/Test) (pull_request) Successful in 46s
CI / Web (dms-browser) (pull_request) Failing after 58s
CI / Web (pdf-viewer) (pull_request) Failing after 43s
Core-Schnittstellen-Vertragstests / contract-tests (pull_request) Successful in 18m38s
CI / Core-Modul (Build/Vet/Test) (push) Successful in 39s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Successful in 3s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Successful in 3s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Failing after 15s
CI / Web (dms-browser) (push) Failing after 51s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (pull_request) Successful in 3s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (pull_request) Successful in 4s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (pull_request) Failing after 14s
CI / Web (shl) (pull_request) Failing after 9s
Supply-Chain-Scan (npm) / npm-audit (pull_request) Failing after 1m16s
Core-Pflichttest-Gate / pflichttest-gate (pull_request) Successful in 18m46s
RBAC-Rollenhierarchie-Pflichtgate / rbac-policy-invariant (pull_request) Successful in 18m59s
/datev/exports braucht connect.datev.write (POST) bzw. connect.datev.read
(GET) mit der Rolle aus X-User-Role. Registriert nur mit
NEXARCH_CONNECT_APP_ARCHIVE_URL; die DATEV-Uebertragung bleibt aus.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 00:23:17 +01:00
patrickandClaude Sonnet 5.5 422e96cc20 connect/statusapi: Rolle aus X-User-Role an die Policy
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Failing after 5s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (pull_request) Failing after 3s
DB-Integrationstests / db-tests (pull_request) Canceled after 0s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Successful in 40s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Successful in 3s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Successful in 3s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Failing after 14s
Supply-Chain-Scan (Go) / govulncheck (push) Failing after 41s
CI / Core-Modul (Build/Vet/Test) (pull_request) Successful in 42s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (pull_request) Successful in 2s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (pull_request) Successful in 2s
CI / Web (dms-browser) (push) Failing after 57s
CI / Web (pdf-viewer) (push) Failing after 40s
CI / Web (shl) (push) Failing after 8s
CI / Web (shl) (pull_request) Failing after 9s
Core-Schnittstellen-Vertragstests / contract-tests (pull_request) Successful in 18m52s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (pull_request) Failing after 14s
CI / Web (dms-browser) (pull_request) Failing after 50s
CI / Web (pdf-viewer) (pull_request) Failing after 38s
Supply-Chain-Scan (Go) / govulncheck (pull_request) Failing after 36s
Supply-Chain-Scan (npm) / npm-audit (pull_request) Failing after 1m17s
Core-Pflichttest-Gate / pflichttest-gate (pull_request) Successful in 18m56s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (pull_request) Failing after 29s
RBAC-Rollenhierarchie-Pflichtgate / rbac-policy-invariant (pull_request) Successful in 18m24s
Bisher fragte statusapi mit fester Rolle user. bearerauth setzt X-User-Role
aus dem Token; ohne Header bleibt es bei user.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-29 23:42:16 +01:00
patrickandClaude Sonnet 5.5 df9581cf45 workflow, connect: bearerauth aus common/bearerauth
DB-Integrationstests / db-tests (pull_request) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Successful in 38s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Successful in 3s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Successful in 2s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Failing after 12s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Failing after 29s
CI / Core-Modul (Build/Vet/Test) (pull_request) Successful in 39s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (pull_request) Successful in 2s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (pull_request) Canceled after 0s
CI / Web (dms-browser) (push) Failing after 52s
CI / Web (pdf-viewer) (push) Failing after 37s
CI / Web (shl) (push) Failing after 8s
CI / Web (dms-browser) (pull_request) Failing after 55s
CI / Web (pdf-viewer) (pull_request) Failing after 39s
CI / Web (shl) (pull_request) Failing after 7s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (pull_request) Successful in 2s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (pull_request) Failing after 14s
common Pflichttest-Gate / pflichttest-gate (pull_request) Successful in 19m7s
Supply-Chain-Scan (Go) / govulncheck (push) Failing after 1m1s
Core-Schnittstellen-Vertragstests / contract-tests (pull_request) Successful in 18m45s
Supply-Chain-Scan (npm) / npm-audit (pull_request) Failing after 1m13s
Core-Pflichttest-Gate / pflichttest-gate (pull_request) Successful in 18m28s
Supply-Chain-Scan (Go) / govulncheck (pull_request) Failing after 38s
RBAC-Rollenhierarchie-Pflichtgate / rbac-policy-invariant (pull_request) Successful in 18m16s
Workflow-Pflichttest-Gate / pflichttest-gate (pull_request) Successful in 18m10s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Failing after 4s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (pull_request) Failing after 3s
Die beiden gleichen Kopien entfallen; common v0.2.2. Das Testpflicht-Muster
fuer bearerauth wandert vom workflow-Gate in das common-Gate.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-29 23:41:06 +01:00
patrick 1da0985ee9 Merge pull request 'docs: Arbeitskarten Portal-Betrieb 131' (#425) from docs/arbeitskarten-131 into main
CI / Web (pdf-viewer) (push) Failing after 39s
CI / Web (shl) (push) Failing after 7s
CI / Core-Modul (Build/Vet/Test) (push) Successful in 39s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Successful in 3s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Successful in 3s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Failing after 13s
CI / Web (dms-browser) (push) Failing after 51s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Failing after 2s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Failing after 29s
2026-10-29 15:48:44 +01:00
patrickandClaude Sonnet 5.5 8db62327dc docs: Arbeitskarten D-02, D-03 aktualisiert, D-08 und D-09 ergaenzt
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (pull_request) Canceled after 0s
CI / Web (pdf-viewer) (pull_request) Canceled after 0s
CI / Web (shl) (pull_request) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
RBAC-Rollenhierarchie-Pflichtgate / rbac-policy-invariant (pull_request) Canceled after 0s
Core-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
Supply-Chain-Scan (npm) / npm-audit (pull_request) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (pull_request) Canceled after 0s
Core-Schnittstellen-Vertragstests / contract-tests (pull_request) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (pull_request) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (pull_request) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (pull_request) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (pull_request) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (pull_request) Canceled after 0s
CI / Web (dms-browser) (pull_request) Canceled after 0s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-29 15:37:03 +01:00
26 changed files with 330 additions and 320 deletions

No files matched your search

+1 -1
View File
@@ -49,7 +49,7 @@ import (
const workerPollInterval = 2 * time.Second
const defaultListenAddr = "127.0.0.1:9600"
const defaultListenAddr = "127.0.0.1:8900"
const defaultModel = "default"
type config struct {
+40
View File
@@ -0,0 +1,40 @@
// Command migrate wendet die offenen Postgres-Migrationen des AI-Moduls an.
// Einmal gegen eine frische Datenbank ausfuehren, bevor ai-app startet.
package main
import (
"context"
"log"
"os"
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
)
func main() {
dsn := os.Getenv("NEXARCH_AI_APP_TENANT_DSN")
if dsn == "" {
log.Fatal("migrate: NEXARCH_AI_APP_TENANT_DSN ist erforderlich")
}
dir := "migrations"
if len(os.Args) > 1 {
dir = os.Args[1]
}
ctx := context.Background()
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
log.Fatalf("migrate: datenbankverbindung: %v", err)
}
defer pool.Close()
migrations, err := migrate.Load(dir)
if err != nil {
log.Fatalf("migrate: migrationen laden: %v", err)
}
applied, err := migrate.Up(ctx, pool, migrations)
if err != nil {
log.Fatalf("migrate: migrationen anwenden: %v", err)
}
log.Printf("migrate: %d migration(en) angewendet", len(applied))
}
+26
View File
@@ -0,0 +1,26 @@
# Betrieb: ai-app
Port-Bereich 8900 bis 8999 (`deploy/ports.tsv`), Routenpräfix `/api/ai/v1` (`deploy/routes.tsv`). Secrets gehören ausschließlich in die Umgebung (`/etc/nexarch/ai-app.env`, Rechte 600), nie in Code, Logs oder Dokumentation.
## Einrichten
1. Datenbank und Rolle anlegen (nicht Teil der Units).
2. `/etc/nexarch/ai-app.env` mit den Variablen unten anlegen.
3. Migrationen anwenden: `NEXARCH_AI_APP_TENANT_DSN=... ./migrate ai/migrations` (Fortschrittstabelle `ai_schema_migrations`).
4. Unit aus `deploy/systemd/nexarch-ai-app.service.tmpl` installieren (`__INSTALL_DIR__` ersetzen), `systemctl enable --now nexarch-ai-app`.
5. Health: `GET /healthz`.
Eine Datenbank, die vor ENG-10 mit den AI-Migrationen angelegt wurde, führte den Fortschritt in `connect_schema_migrations`. Ist sie ausschließlich AI-Datenbank, vor dem ersten Lauf `ALTER TABLE connect_schema_migrations RENAME TO ai_schema_migrations;`. Bei gemeinsamer Datenbank mit Connect nicht umbenennen, sondern die AI-Einträge von Hand in die neue Tabelle übertragen.
## Variablen
| Variable | Standard | Wirkung |
|---|---|---|
| `NEXARCH_AI_APP_TENANT_DSN` | Pflicht | Datenbank des AI-Moduls |
| `NEXARCH_AI_APP_SERVICE_TOKEN` | Pflicht | Service-Token, das alle Aufrufer im Header `X-Service-Token` senden |
| `NEXARCH_AI_APP_LISTEN_ADDR` | `127.0.0.1:8900` | Lauschadresse |
| `NEXARCH_AI_APP_MODEL` | `default` | Modellname für Inferenzaufrufe |
| `NEXARCH_AI_APP_POLICY_URL` / `_POLICY_SERVICE_TOKEN` | Pflicht, oder Übergangsschalter | Rechteprüfung der semantischen Suche; Token Pflicht, sobald URL gesetzt |
| `NEXARCH_AI_APP_POLICY_DISABLED` | aus | `1` startet ohne Rechteprüfung (Übergang, Warnlog) |
| `NEXARCH_AI_APP_MODULE_BASE_URLS` | leer | `modul=url,modul2=url2`, Basis-URLs für den Rückschreib-Push |
| `NEXARCH_AI_APP_CORE_BASE_URL` / `_CORE_SERVICE_TOKEN` | leer | Core-Anbindung; ohne sie ist die Objekttyp-Registrierung (GOV-01) deaktiviert |
+40
View File
@@ -0,0 +1,40 @@
# ENG-10 – ai-app: Deployment-Anbindung – Prüfprotokoll
Datum: 2026-11-06. Host: Entwicklungshost 192.168.1.204 (Wegwerf-Rolle/DB `eng10_test`, nach Abschluss entfernt).
## Befund (Ausgangslage)
- `ai-app` lauschte standardmäßig auf `127.0.0.1:9600`, außerhalb des für `ai` reservierten Bereichs 8900 bis 8999 (`deploy/ports.tsv`). 9600 belegt außerdem `workflow-app` (`docs/BETRIEB-PORTAL.md`). `scripts/check-port-defaults.sh` erkennt nur `":NNNN"`-Literale und meldete den Verstoß nicht.
- Kein systemd-Template für `ai-app` (Connect und DMS haben eines).
- Kein Migrationswerkzeug für das AI-Modul; `ai/internal/migrate` führte den Fortschritt in der Tabelle `connect_schema_migrations` (Kopierfehler aus dem Connect-Modul).
## Umsetzung
- Standard-Lauschadresse `127.0.0.1:8900`.
- `ai/cmd/migrate` (Muster `connect/cmd/migrate`, DSN aus `NEXARCH_AI_APP_TENANT_DSN`, Verzeichnis als Argument).
- Fortschrittstabelle `ai_schema_migrations`.
- `deploy/systemd/nexarch-ai-app.service.tmpl` nach dem Muster von `nexarch-connect-app` (gleiche Härtung).
- `ai/docs/BETRIEB-UMGEBUNGSVARIABLEN.md` mit Einrichtungsschritten und Variablen.
## Akzeptanzkriterien
| # | Kriterium | Ergebnis |
|---|---|---|
| 1 | Standard-Port liegt im Bereich von `ai` | **Erfüllt.** 8900. `scripts/check-port-defaults.sh` meldet für `ai/cmd/ai-app` nichts (dort erkennt es allerdings das Format `127.0.0.1:NNNN` ohnehin nicht, siehe Einschränkungen). |
| 2 | systemd-Template vorhanden und gehärtet | **Erfüllt.** `scripts/check-systemd-hardening.sh`: ok, 18 Dienste geprüft. Start der Unit selbst nicht getestet. |
| 3 | Migrationen anwendbar, idempotent | **Erfüllt.** Frische DB: 16 Migrationen angewendet, zweiter Lauf 0; ohne DSN klare Fehlermeldung, Abbruch. |
| 4 | Eigene Fortschrittstabelle | **Erfüllt.** `ai_schema_migrations` existiert, `connect_schema_migrations` nicht (`TestUp_UsesOwnTrackingTable`, echte DB). |
## Prüfungen vor der Abnahme
| # | Prüfung | Ergebnis |
|---|---|---|
| 1 | Migrationswerkzeug gegen frische Wegwerf-DB | **Bestanden** (siehe AK3). |
| 2 | Bestehende Modultests | **Bestanden.** `./internal/migrate/`, `./cmd/ai-app/` grün, `go build ./...` und `go vet` sauber. |
| 3 | Unit startet `ai-app` real | **Offen.** Template nicht auf einem Host installiert, `systemctl` nicht ausgeführt. |
## Einschränkungen
- Umbenennung der Fortschrittstabelle betrifft bestehende AI-Datenbanken; Hinweis in `BETRIEB-UMGEBUNGSVARIABLEN.md`. Eine automatische Übernahme gibt es nicht.
- Nicht Teil der Kachel: Aufnahme in `deploy/rollout-131-*.sh` (Host-131-Rollout), Provisionierung von DB, Rolle und Secrets.
- Weitere Port-Verstöße außerhalb von `ai`, die beim Prüfen auffielen und nicht behoben wurden: `workflow-app` und `tasks-a11y-fixture` auf 9600 (Bereich 8800 bis 8899), Connect-Fixtures doppelt auf 9500, mehrere Fixtures und `verapdf-sidecar` außerhalb ihrer Bereiche. `check-port-defaults.sh` erkennt `127.0.0.1:NNNN`-Literale nicht; eine Erweiterung würde diese Verstöße sichtbar machen und braucht eine eigene Entscheidung.
+1 -1
View File
@@ -12,7 +12,7 @@ import (
type Migration = cmigrate.Migration
const TrackingTable = "connect_schema_migrations"
const TrackingTable = "ai_schema_migrations"
var migrator = cmigrate.MustNew(TrackingTable)
+41
View File
@@ -0,0 +1,41 @@
package migrate
import (
"context"
"os"
"path/filepath"
"testing"
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
)
func TestUp_UsesOwnTrackingTable(t *testing.T) {
dsn := os.Getenv("TEST_TENANT_DSN")
if dsn == "" {
t.Skip("TEST_TENANT_DSN nicht gesetzt, Integrationstest uebersprungen")
}
pool := testdb.Pool(t, dsn)
dir, err := filepath.Abs("../../migrations")
if err != nil {
t.Fatal(err)
}
migrations, err := Load(dir)
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
if _, err := Up(ctx, pool, migrations); err != nil {
t.Fatal(err)
}
again, err := Up(ctx, pool, migrations)
if err != nil || len(again) != 0 {
t.Fatalf("zweiter lauf soll nichts anwenden: %v %v", again, err)
}
var own, foreign bool
if err := pool.QueryRow(ctx, `SELECT to_regclass('ai_schema_migrations') IS NOT NULL, to_regclass('connect_schema_migrations') IS NOT NULL`).Scan(&own, &foreign); err != nil {
t.Fatal(err)
}
if !own || foreign {
t.Fatalf("fortschrittstabelle: ai=%v connect=%v", own, foreign)
}
}
+1
View File
@@ -20,6 +20,7 @@ var sensitive = []*regexp.Regexp{
regexp.MustCompile(`(^|/)common/policyclient/`),
regexp.MustCompile(`(^|/)common/apierror/`),
regexp.MustCompile(`(^|/)common/httpharden/`),
regexp.MustCompile(`(^|/)common/bearerauth/`),
}
func main() {
+26 -1
View File
@@ -15,12 +15,15 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/common/bearerauth"
"gitea.perlbach24.de/scripte/nexarch/common/moduleauth"
"gitea.perlbach24.de/scripte/nexarch/common/policyclient"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/bearerauth"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/archiveclient"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/connectorsetupapi"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/credential"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/crypto"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/datevapi"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/datevexport"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/dms2import"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/dms2importapi"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/fieldmapping"
@@ -56,6 +59,25 @@ func buildPolicyClient() *policyclient.Client {
return policyclient.New(policyURL, token)
}
// buildDatevExports liefert nil ohne NEXARCH_CONNECT_APP_ARCHIVE_URL; dann ist
// /datev/exports nicht registriert. Die DATEV-Uebertragung ist hier nicht verdrahtet.
func buildDatevExports(pool *pgxpool.Pool) *datevexport.Store {
archiveURL := os.Getenv("NEXARCH_CONNECT_APP_ARCHIVE_URL")
if archiveURL == "" {
log.Print("connect-app: NEXARCH_CONNECT_APP_ARCHIVE_URL fehlt, /datev/exports nicht registriert")
return nil
}
token := os.Getenv("NEXARCH_CONNECT_APP_ARCHIVE_SERVICE_TOKEN")
if token == "" {
log.Fatal("connect-app: NEXARCH_CONNECT_APP_ARCHIVE_SERVICE_TOKEN ist erforderlich, wenn NEXARCH_CONNECT_APP_ARCHIVE_URL gesetzt ist")
}
return &datevexport.Store{
Pool: pool,
Archive: archiveclient.New(archiveURL, token),
CallbackURL: os.Getenv("NEXARCH_CONNECT_APP_DATEV_CALLBACK_URL"),
}
}
func main() {
if !killswitch.Enabled() {
log.Printf("connect-app: deaktiviert (%s=false), kein Start", killswitch.EnvEnabled)
@@ -98,6 +120,9 @@ func main() {
statusapi.NewServer(syncjob.NewStore(pool)).WithPolicy(policy).RegisterRoutes(mux)
dms2importapi.NewServer(mappings, dms2import.NewStore(pool)).RegisterRoutes(mux)
connectorsetupapi.NewServer(credentials, mappings).WithPolicy(policy).RegisterRoutes(mux)
if exports := buildDatevExports(pool); exports != nil {
datevapi.NewServer(exports, nil).WithPolicy(policy).RegisterRoutes(mux)
}
srv := &http.Server{
Addr: addr,
+1 -1
View File
@@ -5,7 +5,7 @@ go 1.25.0
toolchain go1.25.13
require (
gitea.perlbach24.de/scripte/nexarch/common v0.2.1
gitea.perlbach24.de/scripte/nexarch/common v0.2.2
github.com/jackc/pgx/v5 v5.9.2
golang.org/x/text v0.29.0
)
+2 -2
View File
@@ -1,5 +1,5 @@
gitea.perlbach24.de/scripte/nexarch/common v0.2.1 h1:vIreSocK/F4+uid+lZ7u0CSNlPFvA1N7A77k+RhNeAc=
gitea.perlbach24.de/scripte/nexarch/common v0.2.1/go.mod h1:CiEImG++QMt1xZqSMeDJhPiB7sfWTLZOwjS6OY/Iguc=
gitea.perlbach24.de/scripte/nexarch/common v0.2.2 h1:PupmTMm3xysOP8J+zgqRAgwro1XlaQn0njsuUgbqN14=
gitea.perlbach24.de/scripte/nexarch/common v0.2.2/go.mod h1:CiEImG++QMt1xZqSMeDJhPiB7sfWTLZOwjS6OY/Iguc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-71
View File
@@ -1,71 +0,0 @@
// Package bearerauth setzt die Identitaet der Connect-APIs aus einem
// verifizierten Core-Modul-Token. Die Handler lesen weiter X-User-Id und
// X-Tenant-Slug; die Middleware ueberschreibt beide aus den Claims, sodass ein
// Client sie nicht frei setzen kann. X-User-Role kommt nur aus dem Token.
package bearerauth
import (
"encoding/json"
"errors"
"net/http"
"strings"
"gitea.perlbach24.de/scripte/nexarch/common/moduleauth"
)
const (
headerUser = "X-User-Id"
headerTenant = "X-Tenant-Slug"
headerRole = "X-User-Role"
)
// Middleware verlangt fuer Anfragen mit Authorization: Bearer ein gueltiges
// Token. Ohne Bearer werden X-User-Id und X-Tenant-Slug entfernt, ausser
// allowHeaderAuth ist gesetzt (Uebergang, bis alle Clients Bearer senden).
func Middleware(v *moduleauth.Verifier, allowHeaderAuth bool, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.Header.Del(headerRole)
token, ok := bearer(r)
if !ok {
if !allowHeaderAuth {
r.Header.Del(headerUser)
r.Header.Del(headerTenant)
}
next.ServeHTTP(w, r)
return
}
if v == nil {
writeError(w, http.StatusUnauthorized, "nicht angemeldet")
return
}
claims, err := v.Verify(r.Context(), token)
if err != nil {
if errors.Is(err, moduleauth.ErrInvalidToken) {
writeError(w, http.StatusUnauthorized, "nicht angemeldet")
} else {
writeError(w, http.StatusServiceUnavailable, "identitaetsdienst nicht erreichbar")
}
return
}
r.Header.Set(headerUser, claims.Subject)
r.Header.Set(headerTenant, claims.Tenant)
if claims.Role != "" {
r.Header.Set(headerRole, claims.Role)
}
next.ServeHTTP(w, r)
})
}
func bearer(r *http.Request) (string, bool) {
h := r.Header.Get("Authorization")
if len(h) > 7 && strings.EqualFold(h[:7], "bearer ") {
return strings.TrimSpace(h[7:]), true
}
return "", false
}
func writeError(w http.ResponseWriter, status int, message string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(map[string]string{"error": message})
}
@@ -1,75 +0,0 @@
package bearerauth
import (
"net/http"
"net/http/httptest"
"testing"
"gitea.perlbach24.de/scripte/nexarch/common/moduleauth"
)
func echo() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(r.Header.Get("X-User-Id") + "|" + r.Header.Get("X-Tenant-Slug") + "|" + r.Header.Get("X-User-Role")))
})
}
func do(h http.Handler, hdr map[string]string) *httptest.ResponseRecorder {
req := httptest.NewRequest(http.MethodGet, "/x", nil)
for k, v := range hdr {
req.Header.Set(k, v)
}
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
return rec
}
func TestBearerOverridesClientSuppliedHeaders(t *testing.T) {
v, iss := moduleauth.NewTestVerifierAndIssuer()
h := Middleware(v, false, echo())
rec := do(h, map[string]string{
"Authorization": "Bearer " + iss.Token("u1", "acme", "user", "", ""),
"X-User-Id": "fremd", "X-Tenant-Slug": "fremd",
})
if rec.Code != 200 || rec.Body.String() != "u1|acme|user" {
t.Fatalf("code=%d body=%q", rec.Code, rec.Body.String())
}
}
func TestInvalidBearerIs401(t *testing.T) {
v, _ := moduleauth.NewTestVerifierAndIssuer()
_, other := moduleauth.NewTestVerifierAndIssuer()
h := Middleware(v, true, echo())
if rec := do(h, map[string]string{"Authorization": "Bearer " + other.Token("u1", "acme", "user", "", ""), "X-User-Id": "evil"}); rec.Code != 401 {
t.Fatalf("code=%d", rec.Code)
}
if rec := do(Middleware(nil, true, echo()), map[string]string{"Authorization": "Bearer x.y.z"}); rec.Code != 401 {
t.Fatalf("ohne Verifier code=%d", rec.Code)
}
}
func TestWithoutBearerHeadersStrippedUnlessAllowed(t *testing.T) {
v, _ := moduleauth.NewTestVerifierAndIssuer()
hdr := map[string]string{"X-User-Id": "u9", "X-Tenant-Slug": "t9"}
if rec := do(Middleware(v, false, echo()), hdr); rec.Body.String() != "||" {
t.Fatalf("Header muessen entfernt werden, bekam %q", rec.Body.String())
}
if rec := do(Middleware(v, true, echo()), hdr); rec.Body.String() != "u9|t9|" {
t.Fatalf("Uebergangsmodus muss Header durchlassen, bekam %q", rec.Body.String())
}
}
func TestRoleOnlyFromToken(t *testing.T) {
v, iss := moduleauth.NewTestVerifierAndIssuer()
h := Middleware(v, true, echo())
rec := do(h, map[string]string{
"Authorization": "Bearer " + iss.Token("u1", "acme", "tenant_admin", "", ""),
"X-User-Role": "tenant_admin_fremd",
})
if rec.Body.String() != "u1|acme|tenant_admin" {
t.Fatalf("Rolle muss aus dem Token kommen, bekam %q", rec.Body.String())
}
if rec := do(h, map[string]string{"X-User-Role": "tenant_admin"}); rec.Body.String() != "||" {
t.Fatalf("Rolle ohne Token muss entfernt werden, bekam %q", rec.Body.String())
}
}
+33 -2
View File
@@ -9,6 +9,7 @@ import (
"net/http"
"time"
"gitea.perlbach24.de/scripte/nexarch/common/policyclient"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/datevexport"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/datevtransfer"
)
@@ -16,6 +17,12 @@ import (
type Server struct {
Exports *datevexport.Store
Transfers *datevtransfer.Store
policy *policyclient.Client
}
func (s *Server) WithPolicy(client *policyclient.Client) *Server {
s.policy = client
return s
}
func NewServer(exports *datevexport.Store, transfers *datevtransfer.Store) *Server {
@@ -23,8 +30,32 @@ func NewServer(exports *datevexport.Store, transfers *datevtransfer.Store) *Serv
}
func (s *Server) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("POST /datev/exports", s.requireIdentity(s.handleTrigger))
mux.HandleFunc("GET /datev/exports", s.requireIdentity(s.handleList))
mux.HandleFunc("POST /datev/exports", s.requireIdentity(s.requirePermission("connect.datev.write", s.handleTrigger)))
mux.HandleFunc("GET /datev/exports", s.requireIdentity(s.requirePermission("connect.datev.read", s.handleList)))
}
func (s *Server) requirePermission(permission string, next func(w http.ResponseWriter, r *http.Request, tenantSlug string)) func(w http.ResponseWriter, r *http.Request, tenantSlug string) {
return func(w http.ResponseWriter, r *http.Request, tenantSlug string) {
if s.policy != nil {
allowed, err := s.policy.Allowed(r.Context(), tenantSlug, roleOf(r), permission)
if err != nil {
writeError(w, http.StatusServiceUnavailable, "berechtigungsdienst nicht erreichbar")
return
}
if !allowed {
writeError(w, http.StatusForbidden, "keine berechtigung")
return
}
}
next(w, r, tenantSlug)
}
}
func roleOf(r *http.Request) string {
if role := r.Header.Get("X-User-Role"); role != "" {
return role
}
return "user"
}
func (s *Server) requireIdentity(next func(w http.ResponseWriter, r *http.Request, tenantSlug string)) http.HandlerFunc {
@@ -0,0 +1,46 @@
package datevapi
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"gitea.perlbach24.de/scripte/nexarch/common/policyclient"
)
func TestPolicy_PermissionAndRoleReachCore(t *testing.T) {
type ask struct{ role, permission string }
var got []ask
policyAPI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var body struct {
Role string `json:"role"`
Permission string `json:"permission"`
}
_ = json.NewDecoder(r.Body).Decode(&body)
got = append(got, ask{body.Role, body.Permission})
_ = json.NewEncoder(w).Encode(map[string]bool{"allowed": false})
}))
defer policyAPI.Close()
mux := http.NewServeMux()
(&Server{}).WithPolicy(policyclient.New(policyAPI.URL, "tok")).RegisterRoutes(mux)
for _, c := range []struct{ method, role string }{{"GET", "tenant_admin"}, {"POST", ""}} {
req := httptest.NewRequest(c.method, "/datev/exports", strings.NewReader("{}"))
req.Header.Set("X-User-Id", "u1")
req.Header.Set("X-Tenant-Slug", "acme")
if c.role != "" {
req.Header.Set("X-User-Role", c.role)
}
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusForbidden {
t.Fatalf("%s: erwartet 403, bekam %d", c.method, rec.Code)
}
}
want := []ask{{"tenant_admin", "connect.datev.read"}, {"user", "connect.datev.write"}}
if len(got) != 2 || got[0] != want[0] || got[1] != want[1] {
t.Fatalf("erwartet %v, bekam %v", want, got)
}
}
+9 -3
View File
@@ -22,8 +22,7 @@ type Server struct {
// WithPolicy aktiviert RBAC-12: echte Rechtepruefung gegen Core (POST
// /authorize), zusaetzlich zur bestehenden X-User-Id/X-Tenant-Slug-Pruefung.
// Rolle ist mangels eigenem Rollenkonzept in Connect fest "user" (Uebergang,
// wie beim restlichen Header-basierten Identitaetsmodell dieses Pakets).
// Die Rolle kommt aus X-User-Role (setzt bearerauth aus dem Token), sonst "user".
func (s *Server) WithPolicy(client *policyclient.Client) *Server {
s.policy = client
return s
@@ -40,7 +39,7 @@ func (s *Server) RegisterRoutes(mux *http.ServeMux) {
func (s *Server) requirePermission(permission string, next func(w http.ResponseWriter, r *http.Request, tenantSlug string)) func(w http.ResponseWriter, r *http.Request, tenantSlug string) {
return func(w http.ResponseWriter, r *http.Request, tenantSlug string) {
if s.policy != nil {
allowed, err := s.policy.Allowed(r.Context(), tenantSlug, "user", permission)
allowed, err := s.policy.Allowed(r.Context(), tenantSlug, roleOf(r), permission)
if err != nil {
writeError(w, http.StatusServiceUnavailable, "berechtigungsdienst nicht erreichbar")
return
@@ -54,6 +53,13 @@ func (s *Server) requirePermission(permission string, next func(w http.ResponseW
}
}
func roleOf(r *http.Request) string {
if role := r.Header.Get("X-User-Role"); role != "" {
return role
}
return "user"
}
func (s *Server) requireIdentity(next func(w http.ResponseWriter, r *http.Request, tenantSlug string)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-User-Id") == "" {
@@ -60,3 +60,32 @@ func TestHandleList_PolicyAllows_Passes(t *testing.T) {
t.Fatalf("erwartet 200 mit berechtigung, bekam %d: %s", rec.Code, rec.Body.String())
}
}
func TestHandleList_RoleFromHeaderReachesPolicy(t *testing.T) {
var got []string
policyAPI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var body struct {
Role string `json:"role"`
}
_ = json.NewDecoder(r.Body).Decode(&body)
got = append(got, body.Role)
_ = json.NewEncoder(w).Encode(map[string]bool{"allowed": true})
}))
defer policyAPI.Close()
s := newTestServer(t).WithPolicy(policyclient.New(policyAPI.URL, "test-token"))
mux := http.NewServeMux()
s.RegisterRoutes(mux)
for _, role := range []string{"tenant_admin", ""} {
req := httptest.NewRequest("GET", "/connectors/status", nil)
req.Header.Set("X-User-Id", "u1")
req.Header.Set("X-Tenant-Slug", "acme")
if role != "" {
req.Header.Set("X-User-Role", role)
}
mux.ServeHTTP(httptest.NewRecorder(), req)
}
if len(got) != 2 || got[0] != "tenant_admin" || got[1] != "user" {
t.Fatalf("erwartet [tenant_admin user], bekam %v", got)
}
}
@@ -0,0 +1,23 @@
[Unit]
Description=NEXARCH AI - ai-app (ENG-10)
After=network.target postgresql.service
Wants=postgresql.service
[Service]
Type=simple
User=nexarch
Group=nexarch
EnvironmentFile=/etc/nexarch/ai-app.env
ExecStart=__INSTALL_DIR__/bin/ai-app
Restart=on-failure
RestartSec=3
WorkingDirectory=__INSTALL_DIR__
# QA-15 F-13/OPS-08: reiner Netzwerkdienst (HTTP + Postgres-DSN aus
# EnvironmentFile), keine eigene Schreibarbeit im Dateisystem.
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
+4 -2
View File
@@ -71,9 +71,11 @@ Offene PRs, die auf Merge warten, stehen unter C-01. Merge-Reihenfolge: erst DB-
## D. Portal-Betrieb (Stand 2026-10-28)
- D-01 Portal-UI (PR #409) und account-api-Start (PR #410): gemergt.
- D-02 Portal auf 131: gebaut, Unit und nginx/nft offen, siehe `docs/BETRIEB-PORTAL.md`.
- D-03 account-api auf 131: DB-Passwort `nexarch_core` abgleichen (Betreiber), Unit aus Vorlage erneuern.
- D-02 Portal auf 131: eingerichtet mit `deploy/131-portal-einrichten.sh` (#424): Unit `nexarch-web-portal`, Port 8450, CORS am Core, Audience `portal`. Offen: Anmeldung im Browser testen.
- D-03 account-api auf 131: erledigt, Passwort der Rolle `nexarch_core` abgeglichen (2026-10-29). Offen: `NEXARCH_ACCOUNT_REGISTRY_DSN` fehlt (Sperr-Ereignisse nicht im zentralen Audit-Log).
- D-04 204: localhost-Adressen in connect/connect-datev/rbac-admin behoben; Port 3113 offen.
- D-05 Frontends auf Bearer: workflow-approvals, workflow-tasks, workflow-cases, routing-rules, Portal-BFF (PR #412, #413), mail-search (#416), connect-setup, connect-status, datev-export, dms2-import mit `connect-app`-Prüfung (#419); gemeinsamer Helfer `createBearerClient` in `@nexarch/shl` (#417). Offen: Ende-zu-Ende-Test mit Anmeldung auf 204; CORS für `Authorization` an der Connect-API auf 131; `connect` in `NEXARCH_ACCOUNT_MODULE_AUDIENCES`; `NEXARCH_CONNECT_APP_JWKS_URL` und `_TENANT_SLUG` setzen; danach `NEXARCH_WORKFLOW_APP_ALLOW_HEADER_AUTH` und `NEXARCH_CONNECT_APP_ALLOW_HEADER_AUTH` entfernen.
- D-06 Workflow-Demo auf 204 läuft (siehe `docs/BETRIEB-PORTAL.md`).
- D-07 Issue #369 (RBAC in ai-search, workflow-tasks, connect-setup): gemergt (#418, #420). Rolle kommt als `X-User-Role` aus dem Token; `connect.setup.write` nur `tenant_admin`. Dienste starten ohne `NEXARCH_{AI,WORKFLOW,CONNECT}_APP_POLICY_URL` nicht (Übergang: `…_POLICY_DISABLED=1`). Offen: Migration 0029 auf allen Mandanten-DBs einspielen, bevor die Binaries ausgerollt werden; `statusapi` fragt weiter mit fester Rolle `user`; `bearerauth` liegt doppelt (workflow, connect) und gehört nach `common/`; `datevapi` ist in `connect-app` nicht registriert.
- D-08 Login und Portal (#422, #423): nach der Anmeldung direkt ins Portal (`NEXT_PUBLIC_PORTAL_URL`, ohne Rücksprung-URL wegen Open-Redirect); Betriebszustand an den Kacheln, nur für `core.tenant.admin` sichtbar, braucht `PORTAL_STATUS_API_URL`. Offen: Statusdienst-Demo auf 204.
- D-09 Archive-Jobs auf 131 (2026-10-29): `backup-full` scheiterte mit falschem Passwort der Rolle `nexarch_backup`, `backup-incremental` als Folge; `reconcile` und `scrub` zeigten auf die Test-DB `dms_tenant_test` und liefen auf `file_revisions`-Fehler. Behoben: Passwort abgeglichen, DSN und Storage-Pfad auf die DB von `dms-app` (`tenant_rollout_test`), `scrub_state` angelegt. Offen: eigene Leserolle statt `dms_app`; klären, ob `tenant_rollout_test` die endgültige Mandanten-DB ist; Dauer des Backup-Ausfalls unbekannt (Fehlstarts seit mindestens 22.10.).
+1 -1
View File
@@ -16,6 +16,7 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/common/bearerauth"
"gitea.perlbach24.de/scripte/nexarch/common/moduleauth"
"gitea.perlbach24.de/scripte/nexarch/common/policyclient"
"gitea.perlbach24.de/scripte/nexarch/common/tenantregistryclient"
@@ -23,7 +24,6 @@ import (
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/approvalapi"
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/backchannel"
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/backchannelapi"
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/bearerauth"
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/caseapi"
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/caseobj"
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/caseoverview"
+3 -2
View File
@@ -64,8 +64,9 @@ aendert — **jede geaenderte `.go`-Datei muss von einer geaenderten oder neuen
- **Auth-nahe Logik:** `workflow/internal/svcauth/` (Service-Token-Vergleich in
konstanter Zeit), `workflow/internal/moduleadapter/` (prueft `X-Service-Token`
gegen `svcauth.Equal` — der tatsaechliche Durchsetzungspunkt),
`workflow/internal/bearerauth/` (prueft das Bearer-Token der Frontends gegen
die Core-JWKS; `X-User-Id` nur noch ueber den Uebergangsschalter)
`common/bearerauth/` (prueft das Bearer-Token der Frontends gegen die
Core-JWKS; `X-User-Id` nur noch ueber den Uebergangsschalter; Gate im
common-Modul)
- **Tenant-Scoping:** `workflow/internal/tenantregistryclient/` (fragt Core TEN-01,
muss bei Fehler fail-closed bleiben), `workflow/internal/backchannel/` und
`workflow/internal/backchannelapi/` (extern erreichbarer Eingang, der den
+1 -1
View File
@@ -3,7 +3,7 @@ module gitea.perlbach24.de/scripte/nexarch/workflow
go 1.25.0
require (
gitea.perlbach24.de/scripte/nexarch/common v0.2.1
gitea.perlbach24.de/scripte/nexarch/common v0.2.2
github.com/jackc/pgx/v5 v5.9.2
)
+2 -2
View File
@@ -1,5 +1,5 @@
gitea.perlbach24.de/scripte/nexarch/common v0.2.1 h1:vIreSocK/F4+uid+lZ7u0CSNlPFvA1N7A77k+RhNeAc=
gitea.perlbach24.de/scripte/nexarch/common v0.2.1/go.mod h1:CiEImG++QMt1xZqSMeDJhPiB7sfWTLZOwjS6OY/Iguc=
gitea.perlbach24.de/scripte/nexarch/common v0.2.2 h1:PupmTMm3xysOP8J+zgqRAgwro1XlaQn0njsuUgbqN14=
gitea.perlbach24.de/scripte/nexarch/common v0.2.2/go.mod h1:CiEImG++QMt1xZqSMeDJhPiB7sfWTLZOwjS6OY/Iguc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -1,71 +0,0 @@
// Package bearerauth setzt die Identitaet der Workflow-APIs aus einem
// verifizierten Core-Modul-Token. Die Handler lesen weiter X-User-Id und
// X-Tenant-Slug; die Middleware ueberschreibt beide aus den Claims, sodass ein
// Client sie nicht frei setzen kann. X-User-Role kommt nur aus dem Token.
package bearerauth
import (
"encoding/json"
"errors"
"net/http"
"strings"
"gitea.perlbach24.de/scripte/nexarch/common/moduleauth"
)
const (
headerUser = "X-User-Id"
headerTenant = "X-Tenant-Slug"
headerRole = "X-User-Role"
)
// Middleware verlangt fuer Anfragen mit Authorization: Bearer ein gueltiges
// Token. Ohne Bearer werden X-User-Id und X-Tenant-Slug entfernt, ausser
// allowHeaderAuth ist gesetzt (Uebergang, bis alle Clients Bearer senden).
func Middleware(v *moduleauth.Verifier, allowHeaderAuth bool, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.Header.Del(headerRole)
token, ok := bearer(r)
if !ok {
if !allowHeaderAuth {
r.Header.Del(headerUser)
r.Header.Del(headerTenant)
}
next.ServeHTTP(w, r)
return
}
if v == nil {
writeError(w, http.StatusUnauthorized, "nicht angemeldet")
return
}
claims, err := v.Verify(r.Context(), token)
if err != nil {
if errors.Is(err, moduleauth.ErrInvalidToken) {
writeError(w, http.StatusUnauthorized, "nicht angemeldet")
} else {
writeError(w, http.StatusServiceUnavailable, "identitaetsdienst nicht erreichbar")
}
return
}
r.Header.Set(headerUser, claims.Subject)
r.Header.Set(headerTenant, claims.Tenant)
if claims.Role != "" {
r.Header.Set(headerRole, claims.Role)
}
next.ServeHTTP(w, r)
})
}
func bearer(r *http.Request) (string, bool) {
h := r.Header.Get("Authorization")
if len(h) > 7 && strings.EqualFold(h[:7], "bearer ") {
return strings.TrimSpace(h[7:]), true
}
return "", false
}
func writeError(w http.ResponseWriter, status int, message string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(map[string]string{"error": message})
}
@@ -1,75 +0,0 @@
package bearerauth
import (
"net/http"
"net/http/httptest"
"testing"
"gitea.perlbach24.de/scripte/nexarch/common/moduleauth"
)
func echo() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(r.Header.Get("X-User-Id") + "|" + r.Header.Get("X-Tenant-Slug") + "|" + r.Header.Get("X-User-Role")))
})
}
func do(h http.Handler, hdr map[string]string) *httptest.ResponseRecorder {
req := httptest.NewRequest(http.MethodGet, "/x", nil)
for k, v := range hdr {
req.Header.Set(k, v)
}
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
return rec
}
func TestBearerOverridesClientSuppliedHeaders(t *testing.T) {
v, iss := moduleauth.NewTestVerifierAndIssuer()
h := Middleware(v, false, echo())
rec := do(h, map[string]string{
"Authorization": "Bearer " + iss.Token("u1", "acme", "user", "", ""),
"X-User-Id": "fremd", "X-Tenant-Slug": "fremd",
})
if rec.Code != 200 || rec.Body.String() != "u1|acme|user" {
t.Fatalf("code=%d body=%q", rec.Code, rec.Body.String())
}
}
func TestInvalidBearerIs401(t *testing.T) {
v, _ := moduleauth.NewTestVerifierAndIssuer()
_, other := moduleauth.NewTestVerifierAndIssuer()
h := Middleware(v, true, echo())
if rec := do(h, map[string]string{"Authorization": "Bearer " + other.Token("u1", "acme", "user", "", ""), "X-User-Id": "evil"}); rec.Code != 401 {
t.Fatalf("code=%d", rec.Code)
}
if rec := do(Middleware(nil, true, echo()), map[string]string{"Authorization": "Bearer x.y.z"}); rec.Code != 401 {
t.Fatalf("ohne Verifier code=%d", rec.Code)
}
}
func TestWithoutBearerHeadersStrippedUnlessAllowed(t *testing.T) {
v, _ := moduleauth.NewTestVerifierAndIssuer()
hdr := map[string]string{"X-User-Id": "u9", "X-Tenant-Slug": "t9"}
if rec := do(Middleware(v, false, echo()), hdr); rec.Body.String() != "||" {
t.Fatalf("Header muessen entfernt werden, bekam %q", rec.Body.String())
}
if rec := do(Middleware(v, true, echo()), hdr); rec.Body.String() != "u9|t9|" {
t.Fatalf("Uebergangsmodus muss Header durchlassen, bekam %q", rec.Body.String())
}
}
func TestRoleOnlyFromToken(t *testing.T) {
v, iss := moduleauth.NewTestVerifierAndIssuer()
h := Middleware(v, true, echo())
rec := do(h, map[string]string{
"Authorization": "Bearer " + iss.Token("u1", "acme", "tenant_admin", "", ""),
"X-User-Role": "tenant_admin_fremd",
})
if rec.Body.String() != "u1|acme|tenant_admin" {
t.Fatalf("Rolle muss aus dem Token kommen, bekam %q", rec.Body.String())
}
if rec := do(h, map[string]string{"X-User-Role": "tenant_admin"}); rec.Body.String() != "||" {
t.Fatalf("Rolle ohne Token muss entfernt werden, bekam %q", rec.Body.String())
}
}
@@ -18,7 +18,6 @@ import (
// workflow/docs/TESTSTRATEGIE.md Abschnitt 4.
var sensitivePathPatterns = []*regexp.Regexp{
regexp.MustCompile(`(^|/)workflow/internal/moduleadapter/`),
regexp.MustCompile(`(^|/)workflow/internal/bearerauth/`),
regexp.MustCompile(`(^|/)workflow/internal/backchannel/`),
regexp.MustCompile(`(^|/)workflow/internal/backchannelapi/`),
regexp.MustCompile(`(^|/)workflow/internal/dualcheck/`),
@@ -35,7 +35,6 @@ func TestCheckDiff_IgnoresNonSensitivePaths(t *testing.T) {
func TestCheckDiff_CoversAllSensitivePackages(t *testing.T) {
files := []string{
"workflow/internal/moduleadapter/x.go",
"workflow/internal/bearerauth/x.go",
"workflow/internal/backchannel/x.go",
"workflow/internal/backchannelapi/x.go",
"workflow/internal/dualcheck/x.go",
@@ -46,10 +45,3 @@ func TestCheckDiff_CoversAllSensitivePackages(t *testing.T) {
t.Fatalf("erwartet %d verstoesse (je package einer), habe %d: %+v", len(files), len(violations), violations)
}
}
func TestCheckDiff_FlagsBearerAuthChangeWithoutTest(t *testing.T) {
violations := CheckDiff([]string{"workflow/internal/bearerauth/bearerauth.go"})
if len(violations) != 1 || violations[0].Package != "workflow/internal/bearerauth" {
t.Fatalf("erwartet 1 verstoss fuer bearerauth, habe: %+v", violations)
}
}