diff --git a/dms/cmd/app/main.go b/dms/cmd/app/main.go index 5f2a7557..c544c12c 100644 --- a/dms/cmd/app/main.go +++ b/dms/cmd/app/main.go @@ -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" ) diff --git a/dms/cmd/scalebench/main.go b/dms/cmd/scalebench/main.go index 15c5c352..a13f5e16 100644 --- a/dms/cmd/scalebench/main.go +++ b/dms/cmd/scalebench/main.go @@ -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" diff --git a/dms/cmd/search-e2e-fixture/main.go b/dms/cmd/search-e2e-fixture/main.go index caf14011..124a254f 100644 --- a/dms/cmd/search-e2e-fixture/main.go +++ b/dms/cmd/search-e2e-fixture/main.go @@ -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" diff --git a/dms/go.mod b/dms/go.mod index a6a5c82b..30c565a1 100644 --- a/dms/go.mod +++ b/dms/go.mod @@ -5,6 +5,7 @@ go 1.25.0 toolchain go1.25.13 require ( + gitea.perlbach24.de/scripte/nexarch/common v0.1.0 github.com/aws/aws-sdk-go-v2 v1.45.1 github.com/aws/aws-sdk-go-v2/config v1.33.1 github.com/aws/aws-sdk-go-v2/credentials v1.20.1 diff --git a/dms/go.sum b/dms/go.sum index f899777e..bee8a5c4 100644 --- a/dms/go.sum +++ b/dms/go.sum @@ -1,5 +1,7 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +gitea.perlbach24.de/scripte/nexarch/common v0.1.0 h1:biNzFr2LOMZC7b1leY3smQtrMmSMp6IW80OvR4LSjLY= +gitea.perlbach24.de/scripte/nexarch/common v0.1.0/go.mod h1:CiEImG++QMt1xZqSMeDJhPiB7sfWTLZOwjS6OY/Iguc= github.com/aws/aws-sdk-go-v2 v1.45.1 h1:iIoG3NaLhV6UZpPXyPXlDj2I9oS8tV/nMcMnITCC6Ks= github.com/aws/aws-sdk-go-v2 v1.45.1/go.mod h1:bttEH6JqnUL8LepvDVfdrds/fZ5bCIxzpe3abyUrhDU= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20 h1:GPRlPwz40I2B2VrBEASOA3Bi77NyeqejNLkifosX0rs= @@ -81,10 +83,10 @@ github.com/pdfcpu/pdfcpu v0.15.0/go.mod h1:NhG6T7b2EEdToXGD5hj8rmXBWSLCjgljCk5c0 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= @@ -114,14 +116,14 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/dms/internal/apierror/apierror.go b/dms/internal/apierror/apierror.go deleted file mode 100644 index 6b21809b..00000000 --- a/dms/internal/apierror/apierror.go +++ /dev/null @@ -1,149 +0,0 @@ -// Package apierror erzwingt das Core-Fehlerformat (API-01) auf allen DMS-Endpunkten. -package apierror - -import ( - "bytes" - "encoding/json" - "net/http" - "strings" -) - -func CodeForStatus(status int) string { - switch status { - case http.StatusBadRequest: - return "invalid_request" - case http.StatusUnauthorized: - return "unauthenticated" - case http.StatusForbidden: - return "forbidden" - case http.StatusNotFound: - return "not_found" - case http.StatusMethodNotAllowed: - return "method_not_allowed" - case http.StatusConflict: - return "conflict" - case http.StatusGone: - return "gone" - case http.StatusRequestEntityTooLarge: - return "payload_too_large" - case http.StatusUnsupportedMediaType: - return "unsupported_media_type" - case http.StatusUnprocessableEntity: - return "unprocessable" - case http.StatusLocked: - return "locked" - case http.StatusTooManyRequests: - return "rate_limited" - case http.StatusServiceUnavailable: - return "unavailable" - } - if status >= 500 { - return "internal_error" - } - return "invalid_request" -} - -type body struct { - Error struct { - Code string `json:"code"` - Message string `json:"message"` - } `json:"error"` -} - -func Write(w http.ResponseWriter, status int, code, message string) { - var b body - b.Error.Code = code - b.Error.Message = message - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - _ = json.NewEncoder(w).Encode(b) -} - -// Normalize schreibt jede Fehlerantwort (Status >= 400) im Core-Format um; -// Erfolgsantworten laufen ungepuffert durch. -func Normalize(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - rec := &recorder{ResponseWriter: w} - next.ServeHTTP(rec, r) - rec.finish() - }) -} - -type recorder struct { - http.ResponseWriter - status int - buffered bool - buf bytes.Buffer -} - -func (r *recorder) WriteHeader(status int) { - if r.status != 0 { - return - } - r.status = status - if status >= 400 { - r.buffered = true - return - } - r.ResponseWriter.WriteHeader(status) -} - -func (r *recorder) Write(p []byte) (int, error) { - if r.status == 0 { - r.WriteHeader(http.StatusOK) - } - if r.buffered { - return r.buf.Write(p) - } - return r.ResponseWriter.Write(p) -} - -func (r *recorder) Flush() { - if f, ok := r.ResponseWriter.(http.Flusher); ok && !r.buffered { - f.Flush() - } -} - -func (r *recorder) Unwrap() http.ResponseWriter { return r.ResponseWriter } - -func (r *recorder) finish() { - if !r.buffered { - return - } - out := map[string]any{} - message := strings.TrimSpace(r.buf.String()) - code := CodeForStatus(r.status) - - var parsed map[string]any - if json.Unmarshal(r.buf.Bytes(), &parsed) == nil && parsed != nil { - switch e := parsed["error"].(type) { - case string: - message = e - delete(parsed, "error") - out = parsed - case map[string]any: - if c, _ := e["code"].(string); c != "" { - if m, _ := e["message"].(string); m != "" { - r.flush(r.buf.Bytes()) - return - } - } - } - } - if message == "" { - message = http.StatusText(r.status) - } - out["error"] = map[string]string{"code": code, "message": message} - encoded, _ := json.Marshal(out) - r.flush(append(encoded, '\n')) -} - -func (r *recorder) flush(payload []byte) { - h := r.ResponseWriter.Header() - h.Set("Content-Type", "application/json") - h.Del("Content-Length") - h.Del("X-Content-Type-Options") - h.Set("X-Content-Type-Options", "nosniff") - r.ResponseWriter.WriteHeader(r.status) - _, _ = r.ResponseWriter.Write(payload) -} diff --git a/dms/internal/apierror/apierror_test.go b/dms/internal/apierror/apierror_test.go deleted file mode 100644 index a9ae5bb1..00000000 --- a/dms/internal/apierror/apierror_test.go +++ /dev/null @@ -1,118 +0,0 @@ -package apierror - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "strings" - "testing" -) - -func decode(t *testing.T, rec *httptest.ResponseRecorder) map[string]any { - t.Helper() - var m map[string]any - if err := json.Unmarshal(rec.Body.Bytes(), &m); err != nil { - t.Fatalf("keine json-antwort: %q (%v)", rec.Body.String(), err) - } - return m -} - -func errOf(t *testing.T, m map[string]any) (string, string) { - t.Helper() - e, ok := m["error"].(map[string]any) - if !ok { - t.Fatalf("error ist kein objekt: %v", m) - } - c, _ := e["code"].(string) - msg, _ := e["message"].(string) - return c, msg -} - -func TestNormalizeRewritesLegacyErrorShapes(t *testing.T) { - cases := []struct { - name string - handler http.HandlerFunc - status int - wantCode string - wantMsg string - }{ - {"http.Error klartext", func(w http.ResponseWriter, r *http.Request) { http.Error(w, "dokument nicht gefunden", 404) }, 404, "not_found", "dokument nicht gefunden"}, - {"json string", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(422) - _, _ = w.Write([]byte(`{"error":"ungueltiger wert"}`)) - }, 422, "unprocessable", "ungueltiger wert"}, - {"leerer body", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(403) }, 403, "forbidden", "Forbidden"}, - {"5xx", func(w http.ResponseWriter, r *http.Request) { http.Error(w, "boom", 500) }, 500, "internal_error", "boom"}, - } - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - rec := httptest.NewRecorder() - Normalize(c.handler).ServeHTTP(rec, httptest.NewRequest("GET", "/x", nil)) - if rec.Code != c.status { - t.Fatalf("status = %d", rec.Code) - } - if ct := rec.Header().Get("Content-Type"); !strings.HasPrefix(ct, "application/json") { - t.Errorf("content-type = %q", ct) - } - code, msg := errOf(t, decode(t, rec)) - if code != c.wantCode || msg != c.wantMsg { - t.Errorf("code=%q msg=%q, erwartet %q/%q", code, msg, c.wantCode, c.wantMsg) - } - }) - } -} - -func TestNormalizeKeepsExtraFieldsAndCoreShape(t *testing.T) { - rec := httptest.NewRecorder() - Normalize(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(409) - _, _ = w.Write([]byte(`{"error":"duplikat","documentId":"d1"}`)) - })).ServeHTTP(rec, httptest.NewRequest("POST", "/x", nil)) - m := decode(t, rec) - if m["documentId"] != "d1" { - t.Errorf("zusatzfeld verloren: %v", m) - } - if c, _ := errOf(t, m); c != "conflict" { - t.Errorf("code = %q", c) - } - - rec = httptest.NewRecorder() - Normalize(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - Write(w, 401, "unauthenticated", "nicht angemeldet") - })).ServeHTTP(rec, httptest.NewRequest("GET", "/x", nil)) - if c, m := errOf(t, decode(t, rec)); c != "unauthenticated" || m != "nicht angemeldet" { - t.Errorf("core-format veraendert: %q %q", c, m) - } -} - -func TestNormalizeSuccessPassesThroughUnbuffered(t *testing.T) { - rec := httptest.NewRecorder() - Normalize(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/pdf") - _, _ = w.Write([]byte("%PDF-1.7")) - })).ServeHTTP(rec, httptest.NewRequest("GET", "/x", nil)) - if rec.Code != 200 || rec.Body.String() != "%PDF-1.7" || rec.Header().Get("Content-Type") != "application/pdf" { - t.Errorf("erfolgsantwort veraendert: %d %q %q", rec.Code, rec.Body.String(), rec.Header().Get("Content-Type")) - } -} - -func TestNormalizeMuxNotFoundAndMethodNotAllowed(t *testing.T) { - mux := http.NewServeMux() - mux.HandleFunc("GET /a", func(w http.ResponseWriter, r *http.Request) {}) - h := Normalize(mux) - for _, c := range []struct { - method, path string - status int - code string - }{{"GET", "/nope", 404, "not_found"}, {"POST", "/a", 405, "method_not_allowed"}} { - rec := httptest.NewRecorder() - h.ServeHTTP(rec, httptest.NewRequest(c.method, c.path, nil)) - if rec.Code != c.status { - t.Fatalf("%s %s: status %d", c.method, c.path, rec.Code) - } - if code, _ := errOf(t, decode(t, rec)); code != c.code { - t.Errorf("%s %s: code %q", c.method, c.path, code) - } - } -} diff --git a/dms/internal/apiwiring/apiwiring.go b/dms/internal/apiwiring/apiwiring.go index 2c92dc8b..a80b8249 100644 --- a/dms/internal/apiwiring/apiwiring.go +++ b/dms/internal/apiwiring/apiwiring.go @@ -9,7 +9,7 @@ import ( "github.com/jackc/pgx/v5/pgxpool" - "gitea.perlbach24.de/scripte/nexarch/dms/internal/apierror" + "gitea.perlbach24.de/scripte/nexarch/common/apierror" "gitea.perlbach24.de/scripte/nexarch/dms/internal/auditapi" "gitea.perlbach24.de/scripte/nexarch/dms/internal/autoruleapi" "gitea.perlbach24.de/scripte/nexarch/dms/internal/bulkedit" diff --git a/dms/internal/archtest/defaultclient_test.go b/dms/internal/archtest/defaultclient_test.go new file mode 100644 index 00000000..92c29555 --- /dev/null +++ b/dms/internal/archtest/defaultclient_test.go @@ -0,0 +1,51 @@ +package archtest + +import ( + "io/fs" + "os" + "path/filepath" + "regexp" + "strings" + "testing" +) + +// 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) + } +} diff --git a/dms/internal/crypto/kekprovider.go b/dms/internal/crypto/kekprovider.go index 624fae2a..239056b7 100644 --- a/dms/internal/crypto/kekprovider.go +++ b/dms/internal/crypto/kekprovider.go @@ -5,7 +5,7 @@ import ( "encoding/base64" "encoding/json" "fmt" - "gitea.perlbach24.de/scripte/nexarch/dms/internal/httpclient" + "gitea.perlbach24.de/scripte/nexarch/common/httpclient" "net/http" "net/url" ) diff --git a/dms/internal/dbadmission/dbadmission.go b/dms/internal/dbadmission/dbadmission.go index 257fd8eb..ef850a95 100644 --- a/dms/internal/dbadmission/dbadmission.go +++ b/dms/internal/dbadmission/dbadmission.go @@ -13,7 +13,7 @@ import ( "strconv" "time" - "gitea.perlbach24.de/scripte/nexarch/dms/internal/apierror" + "gitea.perlbach24.de/scripte/nexarch/common/apierror" "gitea.perlbach24.de/scripte/nexarch/dms/internal/jobqueue" ) diff --git a/dms/internal/httpclient/httpclient.go b/dms/internal/httpclient/httpclient.go deleted file mode 100644 index 53c9a9f4..00000000 --- a/dms/internal/httpclient/httpclient.go +++ /dev/null @@ -1,29 +0,0 @@ -// Package httpclient liefert den HTTP-Client fuer ausgehende Dienstaufrufe -// (QA-15 F-11): Zeitlimit statt endlosem Warten auf einen haengenden -// Nachbardienst, und keine Weiterleitungen (ein Service-Token wuerde sonst an -// das Weiterleitungsziel mitgesendet). -package httpclient - -import ( - "net/http" - "time" -) - -// DefaultTimeout gilt fuer einen kompletten Aufruf (Verbindung bis Antwortende). -const DefaultTimeout = 15 * time.Second - -var shared = New(DefaultTimeout) - -// Default ist der gemeinsame Client mit DefaultTimeout. -func Default() *http.Client { return shared } - -// New liefert einen Client mit dem angegebenen Zeitlimit, der Weiterleitungen -// nicht folgt (die 3xx-Antwort wird zurueckgegeben). -func New(timeout time.Duration) *http.Client { - return &http.Client{ - Timeout: timeout, - CheckRedirect: func(*http.Request, []*http.Request) error { - return http.ErrUseLastResponse - }, - } -} diff --git a/dms/internal/httpclient/httpclient_test.go b/dms/internal/httpclient/httpclient_test.go deleted file mode 100644 index ea24c6b2..00000000 --- a/dms/internal/httpclient/httpclient_test.go +++ /dev/null @@ -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) - } -} diff --git a/dms/internal/migrationsharelink/migrate_test.go b/dms/internal/migrationsharelink/migrate_test.go index 67bc69aa..3e8e1e6e 100644 --- a/dms/internal/migrationsharelink/migrate_test.go +++ b/dms/internal/migrationsharelink/migrate_test.go @@ -116,7 +116,7 @@ func setupMigratorTest(t *testing.T) (*Migrator, *pgxpool.Pool, string) { { SourceLinkID: "archivdms-share-3-abgelaufen", SourceDocumentID: "archivdms-doc-1", SourceUserID: "archivdms-user-1", TokenHash: hashToken("abgelaufener-token"), - CreatedAt: time.Now().Add(-30 * 24 * time.Hour), ExpiresAt: time.Now().Add(-24 * time.Hour), + CreatedAt: time.Now().Add(-30 * 24 * time.Hour), ExpiresAt: time.Now().Add(-24 * time.Hour), }, }, } diff --git a/dms/internal/moduleauth/middleware.go b/dms/internal/moduleauth/middleware.go index 7db317f0..66af7973 100644 --- a/dms/internal/moduleauth/middleware.go +++ b/dms/internal/moduleauth/middleware.go @@ -6,7 +6,7 @@ import ( "net/http" "strings" - "gitea.perlbach24.de/scripte/nexarch/dms/internal/apierror" + "gitea.perlbach24.de/scripte/nexarch/common/apierror" ) type Identity struct { diff --git a/dms/internal/moduletypeclient/moduletypeclient.go b/dms/internal/moduletypeclient/moduletypeclient.go index 52b4a2a6..181e4984 100644 --- a/dms/internal/moduletypeclient/moduletypeclient.go +++ b/dms/internal/moduletypeclient/moduletypeclient.go @@ -12,7 +12,7 @@ import ( "fmt" "net/http" - "gitea.perlbach24.de/scripte/nexarch/dms/internal/httpclient" + "gitea.perlbach24.de/scripte/nexarch/common/httpclient" ) type Client struct { diff --git a/dms/internal/policyclient/policyclient.go b/dms/internal/policyclient/policyclient.go index 13934d76..0f9ed012 100644 --- a/dms/internal/policyclient/policyclient.go +++ b/dms/internal/policyclient/policyclient.go @@ -12,7 +12,7 @@ import ( "sync" "time" - "gitea.perlbach24.de/scripte/nexarch/dms/internal/apierror" + "gitea.perlbach24.de/scripte/nexarch/common/apierror" "gitea.perlbach24.de/scripte/nexarch/dms/internal/moduleauth" "gitea.perlbach24.de/scripte/nexarch/dms/internal/openapi" ) diff --git a/dms/internal/retentionclient/retentionclient.go b/dms/internal/retentionclient/retentionclient.go index 0fee1fec..c223c5cb 100644 --- a/dms/internal/retentionclient/retentionclient.go +++ b/dms/internal/retentionclient/retentionclient.go @@ -13,7 +13,7 @@ import ( "net/http" "net/url" - "gitea.perlbach24.de/scripte/nexarch/dms/internal/httpclient" + "gitea.perlbach24.de/scripte/nexarch/common/httpclient" "gitea.perlbach24.de/scripte/nexarch/dms/internal/modulecall" ) diff --git a/dms/internal/retentiondestroy/retentiondestroy.go b/dms/internal/retentiondestroy/retentiondestroy.go index dee5ff39..d540044d 100644 --- a/dms/internal/retentiondestroy/retentiondestroy.go +++ b/dms/internal/retentiondestroy/retentiondestroy.go @@ -17,7 +17,7 @@ import ( "github.com/jackc/pgx/v5/pgxpool" - "gitea.perlbach24.de/scripte/nexarch/dms/internal/apierror" + "gitea.perlbach24.de/scripte/nexarch/common/apierror" "gitea.perlbach24.de/scripte/nexarch/dms/internal/audittrail" "gitea.perlbach24.de/scripte/nexarch/dms/internal/jobqueue" "gitea.perlbach24.de/scripte/nexarch/dms/internal/moduleauth" diff --git a/dms/internal/storage/usagereport.go b/dms/internal/storage/usagereport.go index 7709f549..ed83fa73 100644 --- a/dms/internal/storage/usagereport.go +++ b/dms/internal/storage/usagereport.go @@ -5,7 +5,7 @@ import ( "context" "encoding/json" "fmt" - "gitea.perlbach24.de/scripte/nexarch/dms/internal/httpclient" + "gitea.perlbach24.de/scripte/nexarch/common/httpclient" "net/http" ) diff --git a/dms/internal/tenantregistryclient/tenantregistryclient.go b/dms/internal/tenantregistryclient/tenantregistryclient.go deleted file mode 100644 index 1220e5c1..00000000 --- a/dms/internal/tenantregistryclient/tenantregistryclient.go +++ /dev/null @@ -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) - } -} diff --git a/dms/internal/workflowevent/workflowevent.go b/dms/internal/workflowevent/workflowevent.go index 652a7658..30939846 100644 --- a/dms/internal/workflowevent/workflowevent.go +++ b/dms/internal/workflowevent/workflowevent.go @@ -13,7 +13,7 @@ import ( "net/http" "time" - "gitea.perlbach24.de/scripte/nexarch/dms/internal/httpclient" + "gitea.perlbach24.de/scripte/nexarch/common/httpclient" "gitea.perlbach24.de/scripte/nexarch/dms/internal/jobqueue" ) @@ -21,8 +21,8 @@ const ( ModuleName = "dms" ObjectType = "document" - TriggerCreated = "document.created" - TriggerStatusChanged = "document.status_changed" + TriggerCreated = "document.created" + TriggerStatusChanged = "document.status_changed" TriggerDeadlineReached = "document.deadline_reached" JobType = "workflow_event_publish" diff --git a/dms/internal/workflowevent/workflowevent_test.go b/dms/internal/workflowevent/workflowevent_test.go index d2999982..52810e02 100644 --- a/dms/internal/workflowevent/workflowevent_test.go +++ b/dms/internal/workflowevent/workflowevent_test.go @@ -23,11 +23,11 @@ import ( // bildet dieser Fake den echten, aus dem Quellcode gelesenen HTTP-Vertrag // nach — dokumentiert im PRUEFPROTOKOLL als Grenze dieses Tests. type fakeWorkflow struct { - mu sync.Mutex - token string - registrations []registerRequest - events []publishEventRequest - unreachable bool + mu sync.Mutex + token string + registrations []registerRequest + events []publishEventRequest + unreachable bool rejectAllEvents bool }