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
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>
This commit is contained in:
1 parent
cd5ca8da53
commit
7515b8db4d
96 files changed
+81
-930
No files matched your search
@@ -4,6 +4,7 @@ on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "ai/**"
|
||||
- "common/**"
|
||||
|
||||
jobs:
|
||||
pflichttest-gate:
|
||||
|
||||
@@ -4,6 +4,7 @@ on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "workflow/**"
|
||||
- "common/**"
|
||||
|
||||
jobs:
|
||||
pflichttest-gate:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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"}}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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/`),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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-/
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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/common/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()
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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/common/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)
|
||||
})
|
||||
}
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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,46 +0,0 @@
|
||||
// Package tenantregistryclient ist ein schlanker HTTP-Client fuer Core TEN-01
|
||||
// (Tenant-Registry); DMS ist ein eigenes Go-Modul und kann Cores Pakete nicht
|
||||
// importieren. Gleiches Muster wie archive/internal/tenantregistryclient.
|
||||
package tenantregistryclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
BaseURL string
|
||||
ServiceToken string
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
func New(baseURL, serviceToken string) *Client {
|
||||
return &Client{BaseURL: baseURL, ServiceToken: serviceToken, HTTPClient: &http.Client{Timeout: 10 * time.Second}}
|
||||
}
|
||||
|
||||
// VerifyTenant fragt Core, ob tenantSlug als Tenant gefuehrt wird. Transport-
|
||||
// und Serverfehler kommen als Fehler zurueck, nie als "gueltig": der Aufrufer
|
||||
// muss fail-closed reagieren.
|
||||
func (c *Client) VerifyTenant(ctx context.Context, tenantSlug string) (bool, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.BaseURL+"/tenants/"+url.PathEscape(tenantSlug), nil)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("tenantregistryclient: request bauen: %w", err)
|
||||
}
|
||||
req.Header.Set("X-Service-Token", c.ServiceToken)
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("tenantregistryclient: aufruf fehlgeschlagen: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
return true, nil
|
||||
case http.StatusNotFound:
|
||||
return false, nil
|
||||
default:
|
||||
return false, fmt.Errorf("tenantregistryclient: unerwarteter status %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
package httpharden
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestLimitBody_ContentLengthOverLimitIs413(t *testing.T) {
|
||||
called := false
|
||||
h := LimitBody(10, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { called = true }))
|
||||
req := httptest.NewRequest("POST", "/", strings.NewReader(strings.Repeat("a", 11)))
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusRequestEntityTooLarge || called {
|
||||
t.Fatalf("code=%d called=%v", rec.Code, called)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLimitBody_ChunkedOverLimitFailsRead(t *testing.T) {
|
||||
var readErr error
|
||||
h := LimitBody(10, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, readErr = io.ReadAll(r.Body)
|
||||
}))
|
||||
req := httptest.NewRequest("POST", "/", strings.NewReader(strings.Repeat("a", 50)))
|
||||
req.ContentLength = -1
|
||||
h.ServeHTTP(httptest.NewRecorder(), req)
|
||||
if readErr == nil {
|
||||
t.Fatal("lesen ueber dem Limit muss fehlschlagen")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLimitBody_UnderLimitPasses(t *testing.T) {
|
||||
var got string
|
||||
h := LimitBody(10, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
b, _ := io.ReadAll(r.Body)
|
||||
got = string(b)
|
||||
}))
|
||||
h.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("POST", "/", strings.NewReader("abc")))
|
||||
if got != "abc" {
|
||||
t.Fatalf("got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewServer_DefaultsAndStreaming(t *testing.T) {
|
||||
s := NewServer(":0", nil, Options{})
|
||||
if s.ReadHeaderTimeout == 0 || s.ReadTimeout == 0 || s.WriteTimeout == 0 || s.IdleTimeout == 0 {
|
||||
t.Fatalf("Timeouts muessen gesetzt sein: %+v", s)
|
||||
}
|
||||
st := NewServer(":0", nil, Options{StreamingBodies: true, StreamingResponses: true})
|
||||
if st.ReadTimeout != 0 || st.WriteTimeout != 0 || st.ReadHeaderTimeout == 0 || st.IdleTimeout == 0 {
|
||||
t.Fatalf("Streaming: nur Lese-/Schreib-Gesamtzeit 0: %+v", st)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewServer_EnvOverride(t *testing.T) {
|
||||
t.Setenv(EnvReadHeaderTimeout, "3")
|
||||
if got := NewServer(":0", nil, Options{}).ReadHeaderTimeout; got != 3*time.Second {
|
||||
t.Fatalf("got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxBodyBytes_EnvOverride(t *testing.T) {
|
||||
if MaxBodyBytes() != DefaultMaxBodyBytes {
|
||||
t.Fatal("Standard")
|
||||
}
|
||||
t.Setenv(EnvMaxBodyBytes, "2048")
|
||||
if MaxBodyBytes() != 2048 {
|
||||
t.Fatal("Env")
|
||||
}
|
||||
}
|
||||
|
||||
// Slowloris: Verbindung sendet unvollstaendige Header, der Server trennt nach ReadHeaderTimeout.
|
||||
func TestSlowHeadersAreClosed(t *testing.T) {
|
||||
t.Setenv(EnvReadHeaderTimeout, "1")
|
||||
srv := NewServer("127.0.0.1:0", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}), Options{})
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
go srv.Serve(ln)
|
||||
defer srv.Close()
|
||||
conn, err := net.Dial("tcp", ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
if _, err := conn.Write([]byte("GET / HTTP/1.1\r\nHost: x\r\nX-Slow: a")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
start := time.Now()
|
||||
_, err = bufio.NewReader(conn).ReadByte()
|
||||
if err == nil {
|
||||
t.Log("Server antwortete (z. B. 408)")
|
||||
}
|
||||
if time.Since(start) > 4*time.Second {
|
||||
t.Fatalf("Verbindung wurde nicht nach dem Header-Timeout beendet (%v)", time.Since(start))
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/mail/internal/loadtest"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/loadtest"
|
||||
"gitea.perlbach24.de/scripte/nexarch/mail/internal/testdb"
|
||||
)
|
||||
|
||||
|
||||
@@ -31,8 +31,8 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/policyclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/mail/internal/moduleauth"
|
||||
"gitea.perlbach24.de/scripte/nexarch/mail/internal/policyclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/mail/internal/search"
|
||||
"gitea.perlbach24.de/scripte/nexarch/mail/internal/storage"
|
||||
)
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/policyclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/mail/internal/moduleauth"
|
||||
"gitea.perlbach24.de/scripte/nexarch/mail/internal/policyclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/mail/internal/search"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
// Package policyclient fragt Core (RBAC-06, POST /authorize) nach der
|
||||
// Rechteentscheidung; Fehler und Ausfall fuehren immer zur Ablehnung.
|
||||
package policyclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
BaseURL string
|
||||
ServiceToken string
|
||||
HTTP *http.Client
|
||||
}
|
||||
|
||||
func New(baseURL, serviceToken string) *Client {
|
||||
return &Client{BaseURL: strings.TrimRight(baseURL, "/"), ServiceToken: serviceToken, HTTP: &http.Client{Timeout: 5 * time.Second}}
|
||||
}
|
||||
|
||||
func (c *Client) Allowed(ctx context.Context, tenant, role, permission string) (bool, error) {
|
||||
body := map[string]string{"role": role, "permission": permission}
|
||||
if tenant != "" {
|
||||
body["tenant"] = tenant
|
||||
}
|
||||
payload, _ := json.Marshal(body)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL+"/authorize", bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Service-Token", c.ServiceToken)
|
||||
resp, err := c.HTTP.Do(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return false, fmt.Errorf("policy: status %d", resp.StatusCode)
|
||||
}
|
||||
var out struct {
|
||||
Allowed *bool `json:"allowed"`
|
||||
}
|
||||
if err := json.NewDecoder(http.MaxBytesReader(nil, resp.Body, 1<<16)).Decode(&out); err != nil || out.Allowed == nil {
|
||||
return false, fmt.Errorf("policy: antwort ohne entscheidung")
|
||||
}
|
||||
return *out.Allowed, nil
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/mail/internal/loadtest"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/loadtest"
|
||||
"gitea.perlbach24.de/scripte/nexarch/mail/internal/testdb"
|
||||
)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/mail/internal/loadtest"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/loadtest"
|
||||
"gitea.perlbach24.de/scripte/nexarch/mail/internal/testdb"
|
||||
)
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/tenantregistryclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/approval"
|
||||
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/approvalapi"
|
||||
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/backchannel"
|
||||
@@ -33,7 +34,6 @@ import (
|
||||
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/routingruleapi"
|
||||
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/routingsuggestclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/task"
|
||||
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/tenantregistryclient"
|
||||
)
|
||||
|
||||
func requireEnv(name string) string {
|
||||
|
||||
@@ -11,3 +11,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
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/svcauth"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/svcauth"
|
||||
)
|
||||
|
||||
// TenantVerifier prueft einen vom aufrufenden Modul mitgelieferten
|
||||
|
||||
@@ -18,8 +18,10 @@ import (
|
||||
// workflow/docs/TESTSTRATEGIE.md Abschnitt 4.
|
||||
var sensitivePathPatterns = []*regexp.Regexp{
|
||||
regexp.MustCompile(`(^|/)workflow/internal/svcauth/`),
|
||||
regexp.MustCompile(`(^|/)common/svcauth/`),
|
||||
regexp.MustCompile(`(^|/)workflow/internal/moduleadapter/`),
|
||||
regexp.MustCompile(`(^|/)workflow/internal/tenantregistryclient/`),
|
||||
regexp.MustCompile(`(^|/)common/tenantregistryclient/`),
|
||||
regexp.MustCompile(`(^|/)workflow/internal/backchannel/`),
|
||||
regexp.MustCompile(`(^|/)workflow/internal/backchannelapi/`),
|
||||
regexp.MustCompile(`(^|/)workflow/internal/dualcheck/`),
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
// Package svcauth vergleicht Service-Token in konstanter Zeit (Muster aus
|
||||
// archive/internal/svcauth, Lehre aus RET-14/F-08: ohne diesen Schutz
|
||||
// beim ersten Entwurf des Adapters 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,47 +0,0 @@
|
||||
// Package tenantregistryclient ist ein schlanker HTTP-Client fuer Core TEN-01
|
||||
// (Tenant-Registry); Workflow ist ein eigenes Go-Modul und kann Cores Pakete
|
||||
// nicht importieren. Gleiches Muster wie dms/internal/tenantregistryclient
|
||||
// und archive/internal/tenantregistryclient.
|
||||
package tenantregistryclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
BaseURL string
|
||||
ServiceToken string
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
func New(baseURL, serviceToken string) *Client {
|
||||
return &Client{BaseURL: baseURL, ServiceToken: serviceToken, HTTPClient: &http.Client{Timeout: 10 * time.Second}}
|
||||
}
|
||||
|
||||
// VerifyTenant fragt Core, ob tenantSlug als Tenant gefuehrt wird. Transport-
|
||||
// und Serverfehler kommen als Fehler zurueck, nie als "gueltig": der Aufrufer
|
||||
// muss fail-closed reagieren.
|
||||
func (c *Client) VerifyTenant(ctx context.Context, tenantSlug string) (bool, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.BaseURL+"/tenants/"+url.PathEscape(tenantSlug), nil)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("tenantregistryclient: request bauen: %w", err)
|
||||
}
|
||||
req.Header.Set("X-Service-Token", c.ServiceToken)
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("tenantregistryclient: aufruf fehlgeschlagen: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
return true, nil
|
||||
case http.StatusNotFound:
|
||||
return false, nil
|
||||
default:
|
||||
return false, fmt.Errorf("tenantregistryclient: unerwarteter status %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user