Compare commits

...
Author SHA1 Message Date
patrickandClaude Sonnet 5.5 7515b8db4d refactor: weitere identische Pakete nach common/ (svcauth, auditclient, tenantregistryclient, policyclient, httpharden, loadtest, httpclient)
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (pull_request) Canceled after 0s
Archive-Schnittstellen-Vertragstests / contract-tests (push) 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
AI-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
Archive-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (pull_request) Canceled after 0s
Archive-Schnittstellen-Vertragstests / contract-tests (pull_request) Canceled after 0s
Connect-Schnittstellen-Vertragstests / contract-tests (push) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (push) Canceled after 0s
Workflow-Schnittstellen-Vertragstests / contract-tests (push) Canceled after 0s
Connect-Schnittstellen-Vertragstests / contract-tests (pull_request) Canceled after 0s
Core-Schnittstellen-Vertragstests / contract-tests (pull_request) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (pull_request) Canceled after 0s
Mail-Schnittstellen-Vertragstests / contract-tests (pull_request) Canceled after 0s
Mail-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
Workflow-Pflichttest-Gate / pflichttest-gate (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 (pdf-viewer) (pull_request) Canceled after 0s
CI / Web (shl) (pull_request) Canceled after 0s
Mail-Schnittstellen-Vertragstests / contract-tests (push) Canceled after 0s
Supply-Chain-Scan (npm) / npm-audit (pull_request) Canceled after 0s
Core-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
RBAC-Rollenhierarchie-Pflichtgate / rbac-policy-invariant (pull_request) Canceled after 0s
Workflow-Schnittstellen-Vertragstests / contract-tests (pull_request) Canceled after 0s
Nur Pakete, deren Code ohne Kommentare/Importpfade byte-gleich ist:
- httpclient: ai, connect (Root behaelt eigene Kopie wegen strengerem Waechtertest)
- svcauth: archive, ai, workflow
- auditclient: ai, connect, archive
- tenantregistryclient: dms, ai, workflow (archive weicht ab, bleibt)
- policyclient: mail, connect (dms weicht ab, bleibt)
- httpharden: Root, archive (dms weicht ab, bleibt)
- loadtest: mail, ai (archive, connect weichen ab, bleiben)

go.mod (require + replace) fuer ai, connect, workflow; Pflichttest-Gates
von ai/workflow und CI-Pfadfilter um common/ erweitert, damit die
sicherheitskritischen Pakete (svcauth, tenantregistryclient) weiter
gegated werden.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 14:06:07 +01:00
patrickandClaude Sonnet 5.5 cd5ca8da53 refactor: geteiltes Modul common/ (Pilot httpclient, apierror)
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (pull_request) Canceled after 0s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
Archive-Pflichttest-Gate / pflichttest-gate (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
Core-Schnittstellen-Vertragstests / contract-tests (pull_request) Canceled after 0s
Supply-Chain-Scan (npm) / npm-audit (pull_request) Canceled after 0s
Core-Pflichttest-Gate / pflichttest-gate (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 (pdf-viewer) (pull_request) Canceled after 0s
CI / Web (shl) (pull_request) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (push) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (pull_request) Canceled after 0s
Mail-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
RBAC-Rollenhierarchie-Pflichtgate / rbac-policy-invariant (pull_request) Canceled after 0s
- common/httpclient: bisher byte-identisch in dms, mail und archive
- common/apierror: bisher byte-identisch in Root und archive
- Importpfade, go.mod (require + replace), archtest-Ausnahmen und
  CI-Pfadfilter der mail-/archive-Pflichttest-Gates angepasst
- Waechtertest fuer den Standard-Client prueft weiter dms, mail, archive
- dms/internal/apierror bleibt eigenstaendig (5xx-Fix in eigenem PR)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 13:52:24 +01:00
135 changed files with 140 additions and 1590 deletions

No files matched your search

+1
View File
@@ -4,6 +4,7 @@ on:
pull_request:
paths:
- "ai/**"
- "common/**"
jobs:
pflichttest-gate:
@@ -4,6 +4,7 @@ on:
pull_request:
paths:
- "archive/**"
- "common/**"
jobs:
pflichttest-gate:
@@ -4,6 +4,7 @@ on:
pull_request:
paths:
- "mail/**"
- "common/**"
jobs:
pflichttest-gate:
@@ -4,6 +4,7 @@ on:
pull_request:
paths:
- "workflow/**"
- "common/**"
jobs:
pflichttest-gate:
+2
View File
@@ -64,6 +64,8 @@ Kein Modul kopiert die Daten eines anderen — stattdessen ein wiederkehrendes *
Gemeinsamer Nenner aller drei: **Referenz statt Kopie, Adapter statt Direktkopplung.**
Go-`internal/`-Pakete sind zwischen den Modulen nicht sichtbar; identische Querschnittspakete werden deshalb im geteilten Modul `common/` gehalten (per `replace` in den `go.mod` eingebunden, keine eigenen Abhängigkeiten außer der Standardbibliothek). Pilot: `common/httpclient`, `common/apierror`. Fachlich verschiedene Pakete (`audittrail`, `cutoverstage`, `crypto`) bleiben modul-lokal.
Dasselbe Prinzip gilt auch innerhalb eines Moduls, auf Code-Ebene: Hauptdateien/Entry-Points bleiben klein, Fachlogik wandert in eigene Module. Vor einer neuen Funktions-/Modul-Datei wird geprüft, ob dieselbe Funktionalität nicht schon woanders existiert (auch modulübergreifend, z. B. in Core) — keine doppelten Funktionsdateien. Das eigene Kanban-Planungswerkzeug wurde nach genau diesem Muster korrigiert (sieben `kanban.html` enthielten identisches JS/CSS als Volltext-Duplikat, damals in `_shared/kanban.js`/`kanban.css` ausgelagert) — die Boards selbst sind inzwischen komplett aus dem Repo entfernt (Ticket-Historie liegt in der Gitea-PR-Historie), das Prinzip "keine doppelten Funktionsdateien" gilt unverändert für den eigentlichen NEXARCH-Code.
## 6. Technische Architektur (Kurzfassung, Details in eigenen Dokumenten)
+1 -1
View File
@@ -41,9 +41,9 @@ import (
"gitea.perlbach24.de/scripte/nexarch/ai/internal/semanticsearchapi"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/summarize"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/summaryapi"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/tenantregistryclient"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/threadsummarize"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/vectorindex"
"gitea.perlbach24.de/scripte/nexarch/common/tenantregistryclient"
)
const workerPollInterval = 2 * time.Second
+4
View File
@@ -13,3 +13,7 @@ require (
golang.org/x/sync v0.17.0 // indirect
golang.org/x/text v0.29.0 // indirect
)
require gitea.perlbach24.de/scripte/nexarch/common v0.0.0
replace gitea.perlbach24.de/scripte/nexarch/common => ../common
-72
View File
@@ -1,72 +0,0 @@
// Package auditclient ist ein schlanker HTTP-Client fuer Core AUD-08
// (internal/audit, POST /audit/ingest). AI ist ein physisch getrenntes
// Go-Modul und kann Cores internal/audit daher nicht direkt importieren -
// nur ueber dessen HTTP-Schnittstelle. Gleiches Muster wie
// connect/internal/auditclient (ENG-06) und archive/internal/auditclient.
package auditclient
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/httpclient"
)
type Client struct {
BaseURL string
ServiceToken string
HTTPClient *http.Client
}
func New(baseURL, serviceToken string) *Client {
return &Client{BaseURL: baseURL, ServiceToken: serviceToken, HTTPClient: httpclient.Default()}
}
type ingestRequest struct {
TenantSlug string `json:"tenant_slug"`
Actor string `json:"actor"`
Action string `json:"action"`
Target string `json:"target"`
Metadata map[string]any `json:"metadata"`
}
// Ingest meldet EIN Audit-Ereignis an Core. Absichtlich keine
// Update-/Delete-Methode: der Ingest-Endpunkt (AUD-08) ruft denselben
// alleinigen, anfuegenden Schreibpfad wie core-interne Aufrufer auf
// (audit.Log.Record), gegen den Core selbst per Datenbank-Trigger jede
// nachtraegliche Aenderung/Loeschung verweigert (AUD-02).
func (c *Client) Ingest(ctx context.Context, tenantSlug, actor, action, target string, metadata map[string]any) error {
body, err := json.Marshal(ingestRequest{
TenantSlug: tenantSlug, Actor: actor, Action: action, Target: target, Metadata: metadata,
})
if err != nil {
return fmt.Errorf("auditclient: request kodieren: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL+"/audit/ingest", bytes.NewReader(body))
if err != nil {
return fmt.Errorf("auditclient: request bauen: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Service-Token", c.ServiceToken)
resp, err := c.httpClient().Do(req)
if err != nil {
return fmt.Errorf("auditclient: aufruf fehlgeschlagen: %w", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusAccepted {
return fmt.Errorf("auditclient: unerwarteter status %d", resp.StatusCode)
}
return nil
}
func (c *Client) httpClient() *http.Client {
if c.HTTPClient != nil {
return c.HTTPClient
}
return httpclient.Default()
}
@@ -1,48 +0,0 @@
package auditclient
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestIngest_SendsExpectedRequest(t *testing.T) {
var gotToken string
var gotBody ingestRequest
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotToken = r.Header.Get("X-Service-Token")
_ = json.NewDecoder(r.Body).Decode(&gotBody)
w.WriteHeader(http.StatusAccepted)
}))
defer srv.Close()
c := New(srv.URL, "test-token")
err := c.Ingest(context.Background(), "tenant_a", "svc:ai", "ai.decision.classify", "doc:1",
map[string]any{"model": "llama3", "prompt_version": 2})
if err != nil {
t.Fatalf("Ingest: %v", err)
}
if gotToken != "test-token" {
t.Errorf("token = %q, want test-token", gotToken)
}
if gotBody.Action != "ai.decision.classify" || gotBody.Target != "doc:1" {
t.Errorf("unerwarteter request: %+v", gotBody)
}
if gotBody.Metadata["model"] != "llama3" {
t.Errorf("metadata.model = %v, want llama3", gotBody.Metadata["model"])
}
}
func TestIngest_UnexpectedStatusIsError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "zugriff verweigert", http.StatusForbidden)
}))
defer srv.Close()
c := New(srv.URL, "test-token")
if err := c.Ingest(context.Background(), "tenant_a", "svc:ai", "ai.decision.classify", "doc:1", nil); err == nil {
t.Fatal("erwartet fehler bei status 403, habe nil")
}
}
+1 -1
View File
@@ -9,11 +9,11 @@ import (
"strings"
"testing"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/auditclient"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/decisionaudit"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider/mock"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider/ollama"
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
)
var testSchema = Schema{Categories: []string{"rechnung", "vertrag", "kuendigung"}}
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"context"
"fmt"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/auditclient"
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
)
// Decision ist eine einzelne, abgeschlossene KI-gestuetzte Entscheidung
@@ -7,7 +7,7 @@ import (
"net/http/httptest"
"testing"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/auditclient"
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
)
type recordedRequest struct {
@@ -7,10 +7,10 @@ import (
"testing"
"time"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/loadtest"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider/ollama"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/semanticsearch"
"gitea.perlbach24.de/scripte/nexarch/common/loadtest"
)
// QA-08 Pruefung 1 (AK1): echte parallele Chat-Anfragen gegen den realen,
+1 -1
View File
@@ -12,8 +12,8 @@ import (
"gitea.perlbach24.de/scripte/nexarch/ai/internal/docchat"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/semanticsearch"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/svcauth"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/vectorindex"
"gitea.perlbach24.de/scripte/nexarch/common/svcauth"
)
type Server struct {
+1 -1
View File
@@ -15,7 +15,7 @@ import (
"gitea.perlbach24.de/scripte/nexarch/ai/internal/extractvalidate"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/invoiceextract"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/svcauth"
"gitea.perlbach24.de/scripte/nexarch/common/svcauth"
)
type Server struct {
@@ -15,7 +15,7 @@ import (
"gitea.perlbach24.de/scripte/nexarch/ai/internal/extractmapping"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/metadatapushclient"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/svcauth"
"gitea.perlbach24.de/scripte/nexarch/common/svcauth"
)
type Server struct {
-25
View File
@@ -1,25 +0,0 @@
// Package httpclient liefert den HTTP-Client fuer ausgehende Dienstaufrufe
// (Kopie des Musters aus connect/internal/httpclient, ENG-06): Zeitlimit
// statt endlosem Warten, keine Weiterleitungen (ein Service-Token wuerde
// sonst an das Weiterleitungsziel mitgesendet).
package httpclient
import (
"net/http"
"time"
)
const DefaultTimeout = 15 * time.Second
var shared = New(DefaultTimeout)
func Default() *http.Client { return shared }
func New(timeout time.Duration) *http.Client {
return &http.Client{
Timeout: timeout,
CheckRedirect: func(*http.Request, []*http.Request) error {
return http.ErrUseLastResponse
},
}
}
-29
View File
@@ -1,29 +0,0 @@
package httpclient
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestNew_DoesNotFollowRedirects(t *testing.T) {
target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("weiterleitungsziel haette nie erreicht werden duerfen")
}))
defer target.Close()
redirector := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, target.URL, http.StatusFound)
}))
defer redirector.Close()
resp, err := Default().Get(redirector.URL)
if err != nil {
t.Fatalf("Get: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusFound {
t.Fatalf("status = %d, want %d (keine weiterleitung gefolgt)", resp.StatusCode, http.StatusFound)
}
}
-138
View File
@@ -1,138 +0,0 @@
// Package loadtest implementiert die fuer QA-07/QA-08 gemeinsam genutzte
// Last- und Leistungsmessung: parallele Sessions ausfuehren, Latenzen
// aufzeichnen, Durchsatz bestimmen. k6/vegeta sind auf diesem Rechner nicht
// installierbar (Projektregel: keine zusaetzlichen Toolchains/Dienste
// installieren) — echter Ersatz ohne externe Abhaengigkeit: reale
// nebenlaeufige Aufrufe ueber die Go-Standardbibliothek, gegen den echten,
// in dieser Sitzung gebauten Code. Eigenstaendige Kopie aus
// mail/internal/loadtest, da Go-Module keine internal/-Pakete teilen.
package loadtest
import (
"context"
"os"
"runtime"
"sort"
"strconv"
"strings"
"sync"
"time"
)
// Result fasst Latenz- und Durchsatzwerte eines Lasttest-Laufs zusammen.
type Result struct {
Protocol string
Concurrency int
Sessions int
Errors int
Duration time.Duration
Latencies []time.Duration // aufsteigend sortiert
}
// Throughput liefert Sessions pro Sekunde über die Gesamtlaufzeit.
func (r Result) Throughput() float64 {
if r.Duration <= 0 {
return 0
}
return float64(r.Sessions) / r.Duration.Seconds()
}
// Percentile liefert die Latenz am p-ten Perzentil (0.0–1.0).
func (r Result) Percentile(p float64) time.Duration {
if len(r.Latencies) == 0 {
return 0
}
idx := int(p * float64(len(r.Latencies)-1))
if idx < 0 {
idx = 0
}
if idx >= len(r.Latencies) {
idx = len(r.Latencies) - 1
}
return r.Latencies[idx]
}
// Max liefert die höchste gemessene Latenz.
func (r Result) Max() time.Duration {
if len(r.Latencies) == 0 {
return 0
}
return r.Latencies[len(r.Latencies)-1]
}
// Run führt totalSessions Sessions über maximal concurrency parallele
// Worker aus und misst je Session die Latenz von sessionFunc.
func Run(ctx context.Context, protocol string, concurrency, totalSessions int, sessionFunc func(ctx context.Context) error) Result {
var mu sync.Mutex
latencies := make([]time.Duration, 0, totalSessions)
errCount := 0
sem := make(chan struct{}, concurrency)
var wg sync.WaitGroup
start := time.Now()
for i := 0; i < totalSessions; i++ {
sem <- struct{}{}
wg.Add(1)
go func() {
defer wg.Done()
defer func() { <-sem }()
t0 := time.Now()
err := sessionFunc(ctx)
elapsed := time.Since(t0)
mu.Lock()
latencies = append(latencies, elapsed)
if err != nil {
errCount++
}
mu.Unlock()
}()
}
wg.Wait()
duration := time.Since(start)
sort.Slice(latencies, func(i, j int) bool { return latencies[i] < latencies[j] })
return Result{
Protocol: protocol,
Concurrency: concurrency,
Sessions: totalSessions,
Errors: errCount,
Duration: duration,
Latencies: latencies,
}
}
// ResourceSample ist eine Momentaufnahme des Ressourcenverbrauchs DIESES
// Prozesses: Heap über die Go-Runtime, kumulierte CPU-Zeit über
// /proc/self/stat (Linux, kein externes Werkzeug nötig).
type ResourceSample struct {
HeapAllocBytes uint64
CPUTimeSeconds float64
}
func SampleResources() ResourceSample {
var m runtime.MemStats
runtime.ReadMemStats(&m)
return ResourceSample{HeapAllocBytes: m.HeapAlloc, CPUTimeSeconds: readSelfCPUTimeSeconds()}
}
// readSelfCPUTimeSeconds liefert die kumulierte User+System-CPU-Zeit dieses
// Prozesses in Sekunden. Liefert 0, wenn /proc/self/stat nicht lesbar ist
// (z. B. auf Nicht-Linux-Systemen) — Aufrufer behandeln das als "keine
// CPU-Messung verfügbar", kein harter Fehler.
func readSelfCPUTimeSeconds() float64 {
data, err := os.ReadFile("/proc/self/stat")
if err != nil {
return 0
}
fields := strings.Fields(string(data))
if len(fields) < 15 {
return 0
}
utime, err1 := strconv.ParseFloat(fields[13], 64)
stime, err2 := strconv.ParseFloat(fields[14], 64)
if err1 != nil || err2 != nil {
return 0
}
const clockTicksPerSecond = 100 // Linux-Standard-USER_HZ
return (utime + stime) / clockTicksPerSecond
}
@@ -15,7 +15,7 @@ import (
"net/url"
"sort"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/httpclient"
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
)
type Client struct {
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"net/http"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/moduletype"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/svcauth"
"gitea.perlbach24.de/scripte/nexarch/common/svcauth"
)
type Server struct {
+1
View File
@@ -18,6 +18,7 @@ import (
// TESTSTRATEGIE-AI.md Abschnitt 3.
var sensitivePathPatterns = []*regexp.Regexp{
regexp.MustCompile(`(^|/)ai/internal/svcauth/`),
regexp.MustCompile(`(^|/)common/svcauth/`),
regexp.MustCompile(`(^|/)ai/internal/tenantfeature/`),
regexp.MustCompile(`(^|/)ai/internal/semanticsearch/`),
regexp.MustCompile(`(^|/)ai/internal/semanticsearchapi/`),
+1 -1
View File
@@ -16,7 +16,6 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/auditclient"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/decisionaudit"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider"
@@ -25,6 +24,7 @@ import (
"gitea.perlbach24.de/scripte/nexarch/ai/internal/residency"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/tenantfeature"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
)
// realPool liefert einen frischen, migrierten Pool auf der Wegwerf-DB aus
+1 -1
View File
@@ -8,8 +8,8 @@ import (
"time"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/limits"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/loadtest"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider"
"gitea.perlbach24.de/scripte/nexarch/common/loadtest"
)
// QA-08 Pruefung 2 (AK2): realistische parallele Klassifizierungs-/
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"net/http"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/residency"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/svcauth"
"gitea.perlbach24.de/scripte/nexarch/common/svcauth"
)
type Server struct {
@@ -13,7 +13,7 @@ import (
"gitea.perlbach24.de/scripte/nexarch/ai/internal/historysuggest"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/routingfeedback"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/svcauth"
"gitea.perlbach24.de/scripte/nexarch/common/svcauth"
)
type Server struct {
@@ -10,7 +10,7 @@ import (
"net/http"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/semanticsearch"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/svcauth"
"gitea.perlbach24.de/scripte/nexarch/common/svcauth"
)
type Server struct {
+1 -1
View File
@@ -10,8 +10,8 @@ import (
"net/http"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/summarize"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/svcauth"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/threadsummarize"
"gitea.perlbach24.de/scripte/nexarch/common/svcauth"
)
type Server struct {
-20
View File
@@ -1,20 +0,0 @@
// Package svcauth vergleicht Service-Token in konstanter Zeit (eigene Kopie
// je Modul, siehe workflow/internal/svcauth — Go-Module teilen keine
// internal/-Pakete über Modulgrenzen; Lehre aus RET-14/F-08: Schutz beim
// ersten Entwurf einbauen statt nachzuruesten).
package svcauth
import (
"crypto/sha256"
"crypto/subtle"
)
// Equal vergleicht die SHA-256-Werte beider Eingaben in konstanter Zeit.
// Ein leeres erwartetes Token stimmt nie.
func Equal(got, want string) bool {
if want == "" {
return false
}
g, w := sha256.Sum256([]byte(got)), sha256.Sum256([]byte(want))
return subtle.ConstantTimeCompare(g[:], w[:]) == 1
}
+1 -1
View File
@@ -12,9 +12,9 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/complianceapi"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/httpharden"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/rbacclient"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/retentionapi"
"gitea.perlbach24.de/scripte/nexarch/common/httpharden"
)
func main() {
+1 -1
View File
@@ -10,10 +10,10 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/httpharden"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/kennzahlenapi"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/rbacclient"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/retentionapi"
"gitea.perlbach24.de/scripte/nexarch/common/httpharden"
)
func main() {
+1 -1
View File
@@ -13,10 +13,10 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/httpharden"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/legalholdapi"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/moduleadapter"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/tenantregistryclient"
"gitea.perlbach24.de/scripte/nexarch/common/httpharden"
)
func main() {
+1 -1
View File
@@ -10,8 +10,8 @@ import (
"net/http"
"os"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/httpharden"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/restoretest"
"gitea.perlbach24.de/scripte/nexarch/common/httpharden"
)
func main() {
+1 -1
View File
@@ -13,9 +13,9 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/httpharden"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/rbacclient"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/retentionapi"
"gitea.perlbach24.de/scripte/nexarch/common/httpharden"
)
func main() {
+1 -1
View File
@@ -14,8 +14,8 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/httpharden"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/scrub"
"gitea.perlbach24.de/scripte/nexarch/common/httpharden"
)
func main() {
+4
View File
@@ -13,3 +13,7 @@ require (
golang.org/x/sync v0.22.0 // indirect
golang.org/x/text v0.41.0 // indirect
)
require gitea.perlbach24.de/scripte/nexarch/common v0.0.0
replace gitea.perlbach24.de/scripte/nexarch/common => ../common
+1 -1
View File
@@ -33,7 +33,7 @@ func moduleRoot(t *testing.T) string {
// Antwort bzw. der rohe ListenAndServe-Aufruf genau die erlaubte
// Implementierung der Regel selbst ist (API-20/API-21), kein Verstoss.
// Das Archive-Modul hat keine weiteren, physisch verschachtelten Go-Module.
var exempt = []string{"internal/apierror", "internal/httpharden"}
var exempt = []string{"internal/httpharden"}
// TestNoErrorTextLeaksIntoHTTPResponses ist Akzeptanzkriterium 1: kein
// err.Error() direkt in einer HTTP-Antwort, ausser ueber apierror.
@@ -1,70 +0,0 @@
// Package auditclient ist ein schlanker HTTP-Client für Core AUD-08
// (internal/audit, POST /audit/ingest). Archive ist ein physisch
// getrenntes Go-Modul und kann Cores internal/audit daher nicht direkt
// importieren — nur über dessen HTTP-Schnittstelle. Gleiches Muster wie
// archive/internal/notifyclient (CFG-05).
package auditclient
import (
"bytes"
"context"
"encoding/json"
"fmt"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/httpclient"
"net/http"
)
type Client struct {
BaseURL string
ServiceToken string
HTTPClient *http.Client
}
func New(baseURL, serviceToken string) *Client {
return &Client{BaseURL: baseURL, ServiceToken: serviceToken, HTTPClient: httpclient.Default()}
}
type ingestRequest struct {
TenantSlug string `json:"tenant_slug"`
Actor string `json:"actor"`
Action string `json:"action"`
Target string `json:"target"`
Metadata map[string]any `json:"metadata"`
}
// Ingest meldet EIN Audit-Ereignis an Core. Jeder Fehler (Transport,
// Timeout, unerwarteter Status) wird als Fehler zurückgegeben — der
// Aufrufer entscheidet, ob/wie er das nachbehandelt (siehe
// archive/internal/audittrail für den outage-toleranten Wrapper).
func (c *Client) Ingest(ctx context.Context, tenantSlug, actor, action, target string, metadata map[string]any) error {
body, err := json.Marshal(ingestRequest{
TenantSlug: tenantSlug, Actor: actor, Action: action, Target: target, Metadata: metadata,
})
if err != nil {
return fmt.Errorf("auditclient: request kodieren: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL+"/audit/ingest", bytes.NewReader(body))
if err != nil {
return fmt.Errorf("auditclient: request bauen: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Service-Token", c.ServiceToken)
resp, err := c.httpClient().Do(req)
if err != nil {
return fmt.Errorf("auditclient: aufruf fehlgeschlagen: %w", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusAccepted {
return fmt.Errorf("auditclient: unerwarteter status %d", resp.StatusCode)
}
return nil
}
func (c *Client) httpClient() *http.Client {
if c.HTTPClient != nil {
return c.HTTPClient
}
return httpclient.Default()
}
+1 -1
View File
@@ -20,7 +20,7 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/auditclient"
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
)
type Recorder struct {
@@ -11,7 +11,7 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/auditclient"
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
)
func setupOutboxTest(t *testing.T) *pgxpool.Pool {
@@ -13,9 +13,9 @@ import (
"strings"
"testing"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/auditclient"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/audittrail"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/referenceset"
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
"github.com/jackc/pgx/v5/pgxpool"
)
@@ -14,7 +14,7 @@ import (
"net/url"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/ediscovery"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/httpclient"
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
)
const ModuleTypeDMSDocument = "dms_document"
@@ -1,99 +0,0 @@
package httpclient
import (
"io/fs"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
"time"
)
func TestDefaultHasTimeout(t *testing.T) {
if Default().Timeout != DefaultTimeout || DefaultTimeout <= 0 {
t.Fatalf("timeout = %s", Default().Timeout)
}
}
func TestClientGivesUpOnHangingServer(t *testing.T) {
release := make(chan struct{})
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { <-release }))
defer srv.Close()
defer close(release)
start := time.Now()
resp, err := New(200 * time.Millisecond).Get(srv.URL)
if err == nil {
_ = resp.Body.Close()
t.Fatal("kein fehler bei haengendem server")
}
if elapsed := time.Since(start); elapsed > 5*time.Second {
t.Fatalf("aufruf dauerte %s", elapsed)
}
}
func TestClientDoesNotFollowRedirects(t *testing.T) {
target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Error("weiterleitungsziel wurde aufgerufen (Service-Token-Leck)")
}))
defer target.Close()
redirector := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, target.URL, http.StatusTemporaryRedirect)
}))
defer redirector.Close()
req, _ := http.NewRequest(http.MethodPost, redirector.URL, strings.NewReader("x"))
req.Header.Set("X-Service-Token", "geheim")
resp, err := Default().Do(req)
if err != nil {
t.Fatalf("do: %v", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusTemporaryRedirect {
t.Fatalf("status = %d, erwartet 307", resp.StatusCode)
}
}
// Architekturtest: im Produktivcode dieses Moduls gibt es keine Aufrufe ueber
// den Standard-Client ohne Zeitlimit.
func TestNoUnboundedDefaultClientInProductionCode(t *testing.T) {
root, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
t.Fatal(err)
}
forbidden := regexp.MustCompile(`http\.DefaultClient|http\.(Get|Post|Head|PostForm)\(`)
err = filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
if n := d.Name(); n == "node_modules" || n == "testdata" || n == ".git" {
return filepath.SkipDir
}
return nil
}
if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
return nil
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
for i, line := range strings.Split(string(data), "\n") {
if t := strings.TrimSpace(line); strings.HasPrefix(t, "//") {
continue
}
if forbidden.MatchString(line) {
rel, _ := filepath.Rel(root, path)
t.Errorf("%s:%d nutzt den Standard-Client ohne Zeitlimit: %s", rel, i+1, strings.TrimSpace(line))
}
}
return nil
})
if err != nil {
t.Fatal(err)
}
}
-116
View File
@@ -1,116 +0,0 @@
// Package httpharden liefert gehaertete http.Server-Grundeinstellungen
// (Timeouts, Header-Limit) und ein Body-Limit fuer alle Dienste (QA-15 F-05).
// Dieselbe Datei existiert in archive/internal/httpharden und
// dms/internal/httpharden, weil die Module getrennte go.mod-Dateien haben.
package httpharden
import (
"log"
"net/http"
"os"
"strconv"
"time"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/apierror"
)
const (
DefaultReadHeaderTimeout = 10 * time.Second
DefaultReadTimeout = 30 * time.Second
DefaultWriteTimeout = 60 * time.Second
DefaultIdleTimeout = 120 * time.Second
DefaultMaxHeaderBytes = 1 << 20
DefaultMaxBodyBytes = 1 << 20
EnvReadHeaderTimeout = "NEXARCH_HTTP_READ_HEADER_TIMEOUT_SECONDS"
EnvReadTimeout = "NEXARCH_HTTP_READ_TIMEOUT_SECONDS"
EnvWriteTimeout = "NEXARCH_HTTP_WRITE_TIMEOUT_SECONDS"
EnvIdleTimeout = "NEXARCH_HTTP_IDLE_TIMEOUT_SECONDS"
EnvMaxBodyBytes = "NEXARCH_HTTP_MAX_BODY_BYTES"
)
// Options weicht vom Standard ab. Nullwerte bedeuten Standard bzw. Env-Wert;
// StreamingBodies/StreamingResponses setzen ReadTimeout/WriteTimeout auf 0
// (unbegrenzt), damit lange Uploads und Downloads nicht abbrechen. Schutz
// gegen langsame Header bleibt ueber ReadHeaderTimeout und IdleTimeout.
type Options struct {
StreamingBodies bool
StreamingResponses bool
}
func envSeconds(name string, def time.Duration) time.Duration {
v := os.Getenv(name)
if v == "" {
return def
}
n, err := strconv.Atoi(v)
if err != nil || n < 1 || n > 3600 {
log.Fatalf("%s muss eine ganze Zahl zwischen 1 und 3600 sein, war %q", name, v)
}
return time.Duration(n) * time.Second
}
// MaxBodyBytes liest die Body-Obergrenze (Standard 1 MiB, Env
// NEXARCH_HTTP_MAX_BODY_BYTES, 1 KiB bis 1 GiB).
func MaxBodyBytes() int64 {
v := os.Getenv(EnvMaxBodyBytes)
if v == "" {
return DefaultMaxBodyBytes
}
n, err := strconv.ParseInt(v, 10, 64)
if err != nil || n < 1<<10 || n > 1<<30 {
log.Fatalf("%s muss zwischen 1024 und 1073741824 liegen, war %q", EnvMaxBodyBytes, v)
}
return n
}
// NewServer baut einen http.Server mit gehaerteten Timeouts.
func NewServer(addr string, h http.Handler, opt Options) *http.Server {
s := &http.Server{
Addr: addr,
Handler: h,
ReadHeaderTimeout: envSeconds(EnvReadHeaderTimeout, DefaultReadHeaderTimeout),
ReadTimeout: envSeconds(EnvReadTimeout, DefaultReadTimeout),
WriteTimeout: envSeconds(EnvWriteTimeout, DefaultWriteTimeout),
IdleTimeout: envSeconds(EnvIdleTimeout, DefaultIdleTimeout),
MaxHeaderBytes: DefaultMaxHeaderBytes,
}
if opt.StreamingBodies {
s.ReadTimeout = 0
}
if opt.StreamingResponses {
s.WriteTimeout = 0
}
return s
}
// ListenAndServe ersetzt http.ListenAndServe: JSON-API mit Body-Limit und
// (QA-15 F-14) apierror.Normalize aussen um jeden Handler — 5xx-Antworten
// verlieren dadurch automatisch jeden Interna-Text (Datenbank-/Pfadfehler),
// unabhaengig davon, was der einzelne Handler geschrieben hat.
func ListenAndServe(addr string, h http.Handler) error {
return NewServer(addr, apierror.Normalize(LimitBody(MaxBodyBytes(), h)), Options{}).ListenAndServe()
}
// ListenAndServeStreaming fuer Dienste mit grossen Uploads/Downloads: keine
// globale Body-Grenze und keine Lese-/Schreib-Gesamtzeit, nur Header-Timeout.
func ListenAndServeStreaming(addr string, h http.Handler) error {
return NewServer(addr, apierror.Normalize(h), Options{StreamingBodies: true, StreamingResponses: true}).ListenAndServe()
}
// LimitBody begrenzt Request-Bodies. Ein Content-Length ueber dem Limit wird
// vor dem Handler mit 413 abgewiesen, chunked Bodies ueber MaxBytesReader.
func LimitBody(max int64, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.ContentLength > max {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusRequestEntityTooLarge)
_, _ = w.Write([]byte(`{"error":{"code":"payload_too_large","message":"Anfrage zu gross"}}`))
return
}
if r.Body != nil && r.Body != http.NoBody {
r.Body = http.MaxBytesReader(w, r.Body, max)
}
next.ServeHTTP(w, r)
})
}
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/httpclient"
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
"net/http"
"net/url"
)
@@ -12,7 +12,7 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/svcauth"
"gitea.perlbach24.de/scripte/nexarch/common/svcauth"
)
const (
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/svcauth"
"gitea.perlbach24.de/scripte/nexarch/common/svcauth"
)
const (
@@ -10,7 +10,7 @@ import (
"context"
"encoding/json"
"fmt"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/httpclient"
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
"net/http"
)
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"encoding/json"
"errors"
"fmt"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/httpclient"
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
"net/http"
)
@@ -8,7 +8,7 @@ package tenantregistryclient
import (
"context"
"fmt"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/httpclient"
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
"net/http"
"net/url"
)
+1 -1
View File
@@ -17,10 +17,10 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/common/httpharden"
"gitea.perlbach24.de/scripte/nexarch/internal/audit"
"gitea.perlbach24.de/scripte/nexarch/internal/auth"
"gitea.perlbach24.de/scripte/nexarch/internal/authtoken"
"gitea.perlbach24.de/scripte/nexarch/internal/httpharden"
"gitea.perlbach24.de/scripte/nexarch/internal/lockout"
"gitea.perlbach24.de/scripte/nexarch/internal/moduletoken"
"gitea.perlbach24.de/scripte/nexarch/internal/rbac"
+1 -1
View File
@@ -18,10 +18,10 @@ import (
"net/http"
"os"
"gitea.perlbach24.de/scripte/nexarch/common/httpharden"
"gitea.perlbach24.de/scripte/nexarch/internal/audit"
"gitea.perlbach24.de/scripte/nexarch/internal/auditadmin"
"gitea.perlbach24.de/scripte/nexarch/internal/db"
"gitea.perlbach24.de/scripte/nexarch/internal/httpharden"
"gitea.perlbach24.de/scripte/nexarch/internal/svcauth"
)
+1 -1
View File
@@ -5,10 +5,10 @@ import (
"log"
"net/http"
"gitea.perlbach24.de/scripte/nexarch/common/httpharden"
"gitea.perlbach24.de/scripte/nexarch/internal/audit"
"gitea.perlbach24.de/scripte/nexarch/internal/config"
"gitea.perlbach24.de/scripte/nexarch/internal/db"
"gitea.perlbach24.de/scripte/nexarch/internal/httpharden"
"gitea.perlbach24.de/scripte/nexarch/internal/moduledirectory"
"gitea.perlbach24.de/scripte/nexarch/internal/svcauth"
"gitea.perlbach24.de/scripte/nexarch/internal/tenant"
+1 -1
View File
@@ -13,8 +13,8 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/common/httpharden"
"gitea.perlbach24.de/scripte/nexarch/internal/flag"
"gitea.perlbach24.de/scripte/nexarch/internal/httpharden"
"gitea.perlbach24.de/scripte/nexarch/internal/kek"
"gitea.perlbach24.de/scripte/nexarch/internal/moduleregistry"
"gitea.perlbach24.de/scripte/nexarch/internal/tenant"
+1 -1
View File
@@ -16,10 +16,10 @@ import (
"os"
"time"
"gitea.perlbach24.de/scripte/nexarch/common/httpharden"
"gitea.perlbach24.de/scripte/nexarch/internal/adminapi"
"gitea.perlbach24.de/scripte/nexarch/internal/db"
"gitea.perlbach24.de/scripte/nexarch/internal/flag"
"gitea.perlbach24.de/scripte/nexarch/internal/httpharden"
"gitea.perlbach24.de/scripte/nexarch/internal/license"
"gitea.perlbach24.de/scripte/nexarch/internal/svcauth"
"gitea.perlbach24.de/scripte/nexarch/internal/usage"
+1 -1
View File
@@ -10,8 +10,8 @@ import (
"net/http"
"os"
"gitea.perlbach24.de/scripte/nexarch/common/httpharden"
"gitea.perlbach24.de/scripte/nexarch/internal/db"
"gitea.perlbach24.de/scripte/nexarch/internal/httpharden"
"gitea.perlbach24.de/scripte/nexarch/internal/metrics"
)
+1 -1
View File
@@ -12,7 +12,7 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/internal/httpharden"
"gitea.perlbach24.de/scripte/nexarch/common/httpharden"
"gitea.perlbach24.de/scripte/nexarch/internal/notify"
"gitea.perlbach24.de/scripte/nexarch/internal/notifyapi"
"gitea.perlbach24.de/scripte/nexarch/internal/notifyprefs"
+1 -1
View File
@@ -18,8 +18,8 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/common/httpharden"
"gitea.perlbach24.de/scripte/nexarch/internal/auth"
"gitea.perlbach24.de/scripte/nexarch/internal/httpharden"
"gitea.perlbach24.de/scripte/nexarch/internal/moduletrust"
"gitea.perlbach24.de/scripte/nexarch/internal/notifyprefs"
)
+1 -1
View File
@@ -15,8 +15,8 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/common/httpharden"
"gitea.perlbach24.de/scripte/nexarch/internal/flag"
"gitea.perlbach24.de/scripte/nexarch/internal/httpharden"
"gitea.perlbach24.de/scripte/nexarch/internal/policy"
"gitea.perlbach24.de/scripte/nexarch/internal/policyapi"
)
+1 -1
View File
@@ -18,8 +18,8 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/common/httpharden"
"gitea.perlbach24.de/scripte/nexarch/internal/auth"
"gitea.perlbach24.de/scripte/nexarch/internal/httpharden"
"gitea.perlbach24.de/scripte/nexarch/internal/moduletrust"
"gitea.perlbach24.de/scripte/nexarch/internal/rbac"
)
+1 -1
View File
@@ -13,9 +13,9 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/common/httpharden"
"gitea.perlbach24.de/scripte/nexarch/internal/audit"
"gitea.perlbach24.de/scripte/nexarch/internal/flag"
"gitea.perlbach24.de/scripte/nexarch/internal/httpharden"
"gitea.perlbach24.de/scripte/nexarch/internal/moduleregistry"
"gitea.perlbach24.de/scripte/nexarch/internal/resync"
"gitea.perlbach24.de/scripte/nexarch/internal/usage"
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"net/http"
"os"
"gitea.perlbach24.de/scripte/nexarch/internal/httpharden"
"gitea.perlbach24.de/scripte/nexarch/common/httpharden"
"gitea.perlbach24.de/scripte/nexarch/internal/secheaders"
)
+1 -1
View File
@@ -11,8 +11,8 @@ import (
"strconv"
"time"
"gitea.perlbach24.de/scripte/nexarch/common/httpharden"
"gitea.perlbach24.de/scripte/nexarch/internal/db"
"gitea.perlbach24.de/scripte/nexarch/internal/httpharden"
"gitea.perlbach24.de/scripte/nexarch/internal/statuspage"
)
+1 -1
View File
@@ -12,9 +12,9 @@ import (
"net/http"
"os"
"gitea.perlbach24.de/scripte/nexarch/common/httpharden"
"gitea.perlbach24.de/scripte/nexarch/internal/audit"
"gitea.perlbach24.de/scripte/nexarch/internal/db"
"gitea.perlbach24.de/scripte/nexarch/internal/httpharden"
"gitea.perlbach24.de/scripte/nexarch/internal/svcauth"
"gitea.perlbach24.de/scripte/nexarch/internal/tenant"
"gitea.perlbach24.de/scripte/nexarch/internal/tenantadmin"
File renamed without changes.
File renamed without changes.
@@ -12,7 +12,7 @@ import (
"fmt"
"net/http"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/httpclient"
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
)
type Client struct {
+3
View File
@@ -0,0 +1,3 @@
module gitea.perlbach24.de/scripte/nexarch/common
go 1.25.0
File renamed without changes.
@@ -57,15 +57,22 @@ func TestClientDoesNotFollowRedirects(t *testing.T) {
}
}
// Architekturtest: im Produktivcode dieses Moduls gibt es keine Aufrufe ueber
// den Standard-Client ohne Zeitlimit.
// Architekturtest: im Produktivcode der Module, die dieses Paket nutzen, gibt
// es keine Aufrufe ueber den Standard-Client ohne Zeitlimit.
func TestNoUnboundedDefaultClientInProductionCode(t *testing.T) {
root, err := filepath.Abs(filepath.Join("..", ".."))
repoRoot, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
t.Fatal(err)
}
for _, mod := range []string{"common", "dms", "mail", "archive"} {
scanNoDefaultClient(t, repoRoot, filepath.Join(repoRoot, mod))
}
}
func scanNoDefaultClient(t *testing.T, repoRoot, root string) {
t.Helper()
forbidden := regexp.MustCompile(`http\.DefaultClient|http\.(Get|Post|Head|PostForm)\(`)
err = filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
@@ -87,7 +94,7 @@ func TestNoUnboundedDefaultClientInProductionCode(t *testing.T) {
continue
}
if forbidden.MatchString(line) {
rel, _ := filepath.Rel(root, path)
rel, _ := filepath.Rel(repoRoot, path)
t.Errorf("%s:%d nutzt den Standard-Client ohne Zeitlimit: %s", rel, i+1, strings.TrimSpace(line))
}
}
@@ -11,7 +11,7 @@ import (
"strconv"
"time"
"gitea.perlbach24.de/scripte/nexarch/internal/apierror"
"gitea.perlbach24.de/scripte/nexarch/common/apierror"
)
const (
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+4
View File
@@ -13,3 +13,7 @@ require (
golang.org/x/sync v0.17.0 // indirect
golang.org/x/text v0.29.0 // indirect
)
require gitea.perlbach24.de/scripte/nexarch/common v0.0.0
replace gitea.perlbach24.de/scripte/nexarch/common => ../common
@@ -12,7 +12,7 @@ import (
"fmt"
"net/http"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/httpclient"
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
)
type Client struct {
+1 -1
View File
@@ -19,7 +19,7 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/auditclient"
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
)
type Recorder struct {
@@ -11,7 +11,7 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/auditclient"
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
)
func setupOutboxTest(t *testing.T) *pgxpool.Pool {
@@ -26,8 +26,8 @@ import (
"strings"
"time"
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/crypto"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/httpclient"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/registry"
)
@@ -13,7 +13,7 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/auditclient"
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/audittrail"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/credential"
connectcrypto "gitea.perlbach24.de/scripte/nexarch/connect/internal/crypto"
@@ -14,8 +14,8 @@ import (
"io"
"net/http"
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/credential"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/httpclient"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/registry"
)
+1 -1
View File
@@ -13,7 +13,7 @@ import (
"net/http"
"net/url"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/httpclient"
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
)
// tenantKEKResponse mirrors Core internal/kek.tenantKEKResponse (JSON
@@ -14,8 +14,8 @@ import (
"io"
"net/http"
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/credential"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/httpclient"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/registry"
)
-25
View File
@@ -1,25 +0,0 @@
// Package httpclient liefert den HTTP-Client fuer ausgehende Dienstaufrufe
// (Muster aus archive/internal/httpclient, QA-15 F-11): Zeitlimit statt
// endlosem Warten, keine Weiterleitungen (ein Service-Token wuerde sonst an
// das Weiterleitungsziel mitgesendet).
package httpclient
import (
"net/http"
"time"
)
const DefaultTimeout = 15 * time.Second
var shared = New(DefaultTimeout)
func Default() *http.Client { return shared }
func New(timeout time.Duration) *http.Client {
return &http.Client{
Timeout: timeout,
CheckRedirect: func(*http.Request, []*http.Request) error {
return http.ErrUseLastResponse
},
}
}
@@ -1,54 +0,0 @@
package httpclient
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestDefaultHasTimeout(t *testing.T) {
if Default().Timeout != DefaultTimeout || DefaultTimeout <= 0 {
t.Fatalf("timeout = %s", Default().Timeout)
}
}
func TestClientGivesUpOnHangingServer(t *testing.T) {
release := make(chan struct{})
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { <-release }))
defer srv.Close()
defer close(release)
start := time.Now()
resp, err := New(200 * time.Millisecond).Get(srv.URL)
if err == nil {
_ = resp.Body.Close()
t.Fatal("kein fehler bei haengendem server")
}
if elapsed := time.Since(start); elapsed > 5*time.Second {
t.Fatalf("aufruf dauerte %s", elapsed)
}
}
func TestClientDoesNotFollowRedirects(t *testing.T) {
target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Error("weiterleitungsziel wurde aufgerufen (Service-Token-Leck)")
}))
defer target.Close()
redirector := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, target.URL, http.StatusTemporaryRedirect)
}))
defer redirector.Close()
req, _ := http.NewRequest(http.MethodPost, redirector.URL, strings.NewReader("x"))
req.Header.Set("X-Service-Token", "geheim")
resp, err := Default().Do(req)
if err != nil {
t.Fatalf("do: %v", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusTemporaryRedirect {
t.Fatalf("status = %d, erwartet 307", resp.StatusCode)
}
}
@@ -18,8 +18,8 @@ import (
"strings"
"time"
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/credential"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/httpclient"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/registry"
)
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"net/http"
"time"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/policyclient"
"gitea.perlbach24.de/scripte/nexarch/common/policyclient"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/syncjob"
)
@@ -6,7 +6,7 @@ import (
"net/http/httptest"
"testing"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/policyclient"
"gitea.perlbach24.de/scripte/nexarch/common/policyclient"
)
// newFakePolicyAPI steht fuer Core's echten POST /authorize-Vertrag (nimmt
@@ -9,7 +9,7 @@ import (
"testing"
"time"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/auditclient"
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/audittrail"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/credential"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/erpconnector"
+1 -1
View File
@@ -13,7 +13,7 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/auditclient"
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/audittrail"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/migrate"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/testdb"
@@ -15,7 +15,7 @@ import (
"net/url"
"time"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/httpclient"
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
)
type Client struct {
+1 -1
View File
@@ -15,6 +15,7 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/common/tenantregistryclient"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/audittrail"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/authz"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/batchsplit"
@@ -34,7 +35,6 @@ import (
"gitea.perlbach24.de/scripte/nexarch/dms/internal/searchwiring"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/shared"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/storage"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/tenantregistryclient"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/watchfolder"
)
+1 -1
View File
@@ -30,9 +30,9 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
dmscrypto "gitea.perlbach24.de/scripte/nexarch/dms/internal/crypto"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/folder"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/httpclient"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/jobqueue"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/migrate"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/routeprefix"
+1 -1
View File
@@ -23,8 +23,8 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
dmscrypto "gitea.perlbach24.de/scripte/nexarch/dms/internal/crypto"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/httpclient"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/jobqueue"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/migrate"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/routeprefix"
Loaded 100 of 135 files, more files were not shown because too many files have changed in this diff. Show more