Integration: authz.Guard umschliesst den App-Mux, Guard-Regeln fuer /search, /saved-searches, /ocr; Warmup im Such-Lasttest
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, 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
CI / Core-Modul (Build/Vet/Test) (pull_request) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (pull_request) Canceled after 0s
CI / 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
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, 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
CI / Core-Modul (Build/Vet/Test) (pull_request) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (pull_request) Canceled after 0s
CI / 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
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
56b2c22346
commit
e172fdfe53
4 files changed
+86
-4
No files matched your search
+24
-1
@@ -27,6 +27,7 @@ import (
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/routeprefix"
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/searchindex"
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/searchsync"
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/authz"
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/searchwiring"
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/shared"
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/storage"
|
||||
@@ -77,7 +78,15 @@ func main() {
|
||||
log.Print("dms-app: NEXARCH_DMS_APP_WATCHFOLDER_DIR nicht gesetzt, INT-08-Consume-Ordner deaktiviert")
|
||||
}
|
||||
|
||||
handler := logging.Middleware(logger)(metricsReg.Middleware(recoverMiddleware(tracker, mux)))
|
||||
var routes http.Handler = mux
|
||||
if dsn := os.Getenv("NEXARCH_DMS_APP_TENANT_DSN"); dsn != "" {
|
||||
guarded, err := guardMux(mux, dsn, prefix, os.Getenv("NEXARCH_DMS_APP_ADMIN_USER_IDS"))
|
||||
if err != nil {
|
||||
log.Fatalf("dms-app: berechtigungs-guard: %v", err)
|
||||
}
|
||||
routes = guarded
|
||||
}
|
||||
handler := logging.Middleware(logger)(metricsReg.Middleware(recoverMiddleware(tracker, routes)))
|
||||
|
||||
log.Printf("dms-app hoert auf %s (version %s)", addr, shared.Version)
|
||||
if err := http.ListenAndServe(addr, handler); err != nil {
|
||||
@@ -97,6 +106,20 @@ func newMux(prefix string, metricsReg *metrics.Registry) (root, api *http.ServeM
|
||||
return root, routeprefix.Mount(root, prefix)
|
||||
}
|
||||
|
||||
func guardMux(next http.Handler, dsn, prefix, adminIDs string) (http.Handler, error) {
|
||||
pool, err := pgxpool.New(context.Background(), dsn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("datenbankverbindung: %w", err)
|
||||
}
|
||||
admins := map[string]bool{}
|
||||
for _, id := range strings.Split(adminIDs, ",") {
|
||||
if id = strings.TrimSpace(id); id != "" {
|
||||
admins[id] = true
|
||||
}
|
||||
}
|
||||
return authz.Guard(next, &authz.PoolChecker{Pool: pool, Admins: admins}, prefix), nil
|
||||
}
|
||||
|
||||
// recoverMiddleware faengt Panics in Handlern ab und meldet sie zentral
|
||||
// (Akzeptanzkriterium 3), statt den Prozess abstuerzen zu lassen.
|
||||
func recoverMiddleware(tracker *errortracking.Tracker, next http.Handler) http.Handler {
|
||||
|
||||
@@ -88,3 +88,51 @@ func TestSetupSearch_UnreachableIndexReturnsError(t *testing.T) {
|
||||
t.Fatal("erreichbarkeitsfehler des suchindex nicht gemeldet")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGuardMux_RejectsUnauthorizedRequestsOnWiredRoutes(t *testing.T) {
|
||||
dsn, addr := os.Getenv("TEST_TENANT_DSN"), os.Getenv("TEST_MANTICORE_ADDR")
|
||||
if dsn == "" || addr == "" {
|
||||
t.Skip("TEST_TENANT_DSN/TEST_MANTICORE_ADDR nicht gesetzt")
|
||||
}
|
||||
const prefix = "/api/dms/v1"
|
||||
root, api := newMux(prefix, metrics.NewRegistry())
|
||||
if err := setupSearch(api, dsn, addr); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
handler, err := guardMux(root, dsn, prefix, "admin-1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
do := func(method, path, header string) int {
|
||||
req := httptest.NewRequest(method, path, nil)
|
||||
if header != "" {
|
||||
req.Header.Set("X-User-Id", header)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
return rec.Code
|
||||
}
|
||||
|
||||
for _, path := range []string{"/search?q=x", "/saved-searches", "/ocr/review", "/ocr/settings"} {
|
||||
if code := do(http.MethodGet, prefix+path, ""); code != http.StatusUnauthorized {
|
||||
t.Errorf("%s ohne identitaet: %d, erwartet 401", path, code)
|
||||
}
|
||||
}
|
||||
if code := do(http.MethodGet, prefix+"/search?q=x&caller=a", "b"); code != http.StatusUnauthorized {
|
||||
t.Errorf("widerspruechliche identitaet: %d, erwartet 401", code)
|
||||
}
|
||||
if code := do(http.MethodGet, prefix+"/ocr/review?caller=user-1", ""); code != http.StatusForbidden {
|
||||
t.Errorf("ocr/review als nicht-admin: %d, erwartet 403", code)
|
||||
}
|
||||
if code := do(http.MethodPut, prefix+"/ocr/settings?caller=user-1", ""); code != http.StatusForbidden {
|
||||
t.Errorf("ocr/settings schreiben als nicht-admin: %d, erwartet 403", code)
|
||||
}
|
||||
if code := do(http.MethodGet, prefix+"/ocr/review?caller=admin-1", ""); code == http.StatusUnauthorized || code == http.StatusForbidden {
|
||||
t.Errorf("ocr/review als admin abgewiesen: %d", code)
|
||||
}
|
||||
for _, path := range []string{"/healthz", "/metrics"} {
|
||||
if code := do(http.MethodGet, path, ""); code != http.StatusOK {
|
||||
t.Errorf("%s ohne identitaet: %d, erwartet 200", path, code)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,8 +145,8 @@ func needsGuard(method string, seg []string) bool {
|
||||
return len(seg) == 2
|
||||
case "auto-rules":
|
||||
return true
|
||||
case "ocr":
|
||||
return len(seg) == 2 && seg[1] == "settings" && method != http.MethodGet
|
||||
case "ocr", "search", "saved-searches":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -162,7 +162,14 @@ func evaluate(r *http.Request, c Checker, caller, method string, seg []string, w
|
||||
return true, nil
|
||||
}
|
||||
return c.OwnsUploadSession(ctx, caller, seg[1])
|
||||
case "auto-rules", "ocr":
|
||||
case "search", "saved-searches":
|
||||
return true, nil
|
||||
case "ocr":
|
||||
if method == http.MethodGet && len(seg) == 2 && seg[1] == "settings" {
|
||||
return true, nil
|
||||
}
|
||||
return c.IsAdmin(ctx, caller)
|
||||
case "auto-rules":
|
||||
return c.IsAdmin(ctx, caller)
|
||||
case "bulk-edit":
|
||||
var body struct {
|
||||
|
||||
@@ -198,6 +198,10 @@ func TestSearch_ConcurrentRequestsStayUnderResponseTimeLimit(t *testing.T) {
|
||||
index(t, db, searchindex.Doc{Title: fmt.Sprintf("%s dokument %d", tok, i), Body: strings.Repeat("vertrag kuendigung ", 50) + tok, CreatedBy: "u1"})
|
||||
}
|
||||
|
||||
for i := 0; i < 20; i++ {
|
||||
get(t, srv.URL+"/search?caller=u1&q="+tok)
|
||||
}
|
||||
|
||||
const workers, perWorker = 20, 10
|
||||
var mu sync.Mutex
|
||||
var durations []time.Duration
|
||||
|
||||
Reference in new issue
Block a user