From 2e952f111dd95c23dba9969068ab1137fcf0d05b Mon Sep 17 00:00:00 2001 From: sysops Date: Mon, 27 Jul 2026 00:08:43 +0200 Subject: [PATCH] =?UTF-8?q?docs(PROJ-72):=20Feature-Spec=20nachtragen=20+?= =?UTF-8?q?=20Audit-Log-H=C3=A4rtung=20f=C3=BCr=20abgelehnte=20Privilege-C?= =?UTF-8?q?hecks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Feature-Spec, INDEX.md und features.ts für PROJ-72 (Superadmin-Peer-Patch-Fix) ergänzt inkl. QA-Ergebnisse. Zusätzlich: abgelehnte Privilege-Escalation-/ Tenant-Isolation-Versuche (403) werden jetzt als Success:false im Audit-Log protokolliert (vorher nur erfolgreiche Updates) - Härtungspunkt aus QA-Runde. --- features/INDEX.md | 3 +- features/PROJ-72-fix-superadmin-peer-patch.md | 67 +++++++++++++++++++ internal/api/admin_users_handlers.go | 32 +++++++++ src/data/features.ts | 1 + 4 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 features/PROJ-72-fix-superadmin-peer-patch.md diff --git a/features/INDEX.md b/features/INDEX.md index 206ee50..df4b10d 100644 --- a/features/INDEX.md +++ b/features/INDEX.md @@ -87,7 +87,8 @@ | PROJ-69 | Admin-Dashboard Tab-Gruppierung (2-Ebenen-Navigation) | Deployed | [PROJ-69](PROJ-69-admin-tabs-gruppierung.md) | 2026-07-06 | | PROJ-70 | User-Self-Service IMAP-Rückholung (Archiv-Mail zurück ins Postfach) | Deployed | [PROJ-70](PROJ-70-imap-rueckholung-self-service.md) | 2026-07-07 | | PROJ-71 | TLS-Pflicht (optional) für eingehenden SMTP-BCC-Journaling-Kanal | Deployed | [PROJ-71](PROJ-71-smtp-require-tls.md) | 2026-07-08 | +| PROJ-72 | Fix Superadmin kann Passwort/Rolle von Superadmin-Peers nicht ändern (Sicherheitsbug) | Deployed | [PROJ-72](PROJ-72-fix-superadmin-peer-patch.md) | 2026-07-27 | -## Next Available ID: PROJ-72 +## Next Available ID: PROJ-73 diff --git a/features/PROJ-72-fix-superadmin-peer-patch.md b/features/PROJ-72-fix-superadmin-peer-patch.md new file mode 100644 index 0000000..49a695d --- /dev/null +++ b/features/PROJ-72-fix-superadmin-peer-patch.md @@ -0,0 +1,67 @@ +--- +id: PROJ-72 +title: Fix Superadmin kann Passwort/Rolle von Superadmin-Peers nicht ändern (Sicherheitsbug) +status: Deployed +created: 2026-07-26 +--- + +## Problem + +`handleUpdateUser` (`internal/api/admin_users_handlers.go`) blockierte per +SEC-01-Privilegien-Check (`roleLevel(target.Role) >= roleLevel(sess.Role)`) +jede Modifikation an Ziel-Usern mit gleichem oder höherem Rollen-Level wie der +Aufrufer. Da Superadmin das höchste Level ist (`roleLevel = 5`), traf dieser +Vergleich auch Superadmin-vs-Superadmin zu: ein Superadmin konnte weder sich +selbst noch andere Superadmins bearbeiten — u.a. kein Passwort-Reset für +andere Superadmins möglich. + +## Lösung + +Peer-Level-Check (`>=`-Vergleich für Ziel-Rolle und Rollen-Zuweisung) gilt nur +noch für `sess.Role != userstore.RoleSuperAdmin`. Superadmin ist die höchste +Stufe — es gibt keine höhere Rolle, vor der geschützt werden müsste, daher ist +Superadmin von diesem Peer-Check ausgenommen. + +Zusätzlich: abgelehnte Privilege-Escalation-/Tenant-Isolation-Versuche (403) +werden jetzt im Audit-Log als `Success: false` protokolliert (vorher nur +erfolgreiche Updates geloggt) — Härtung, während QA-Verifikation entdeckt. + +## Implementation Notes + +- `internal/api/admin_users_handlers.go` `handleUpdateUser`: + - Peer-Level-Check (Ziel-Rolle + Rollen-Zuweisung) in + `if sess.Role != userstore.RoleSuperAdmin { ... }` gekapselt. + - Audit-Log-Eintrag (`Success: false`) bei: Cross-Tenant-Zugriff verweigert, + Privilegien für Ziel-User-Modifikation fehlen, Privilegien für + Rollen-Zuweisung fehlen. + - `handleCreateUser`: Audit-Log-Eintrag (`Success: false`) bei verweigerter + Rollen-Zuweisung beim Anlegen. +- Tenant-Isolation-Check (SEC-02) unverändert — Superadmin hat `TenantID == + nil`, überspringt diesen Block ohnehin. + +## Acceptance Criteria + +- [x] Superadmin kann eigenes Passwort/Rolle/aktiv-Status ändern (Self-Patch). +- [x] Superadmin kann Passwort/Rolle/aktiv-Status eines anderen Superadmins + ändern (Peer-Patch). +- [x] domain_admin kann weiterhin KEINE Peer- oder höherrangigen User + (inkl. Superadmin) patchen (403). +- [x] Cross-Tenant-Zugriff (IDOR) weiterhin blockiert (403). +- [x] Kein Auth-Bypass (401 ohne gültigen Token). +- [x] Abgelehnte Privilege-Escalation-Versuche erscheinen im Audit-Log + (`Success: false`). + +## QA Test Results (2026-07-27, gegen 192.168.1.132) + +| TC | Erwartung | Ergebnis | +|---|---|---| +| Superadmin patcht Peer-Superadmin (Rolle+Passwort+aktiv) | 200 | PASS | +| Superadmin Self-Patch | 200 | PASS | +| domain_admin patcht Superadmin (global) | 403 | PASS | +| domain_admin patcht Peer-domain_admin (gleicher Tenant) | 403 | PASS | +| domain_admin patcht User aus fremdem Tenant (IDOR) | 403 | PASS | +| PATCH ohne JWT | 401 | PASS | +| domain_admin weist eigenem Tenant-User Rolle "admin" zu | 403 | PASS | +| domain_admin patcht regulären User im eigenen Tenant (Positivkontrolle) | 200 | PASS | + +Deployed auf 131 (Produktiv) und 132 (teilproduktiv) am 2026-07-26/27. diff --git a/internal/api/admin_users_handlers.go b/internal/api/admin_users_handlers.go index dad523e..f2c0af0 100644 --- a/internal/api/admin_users_handlers.go +++ b/internal/api/admin_users_handlers.go @@ -66,6 +66,14 @@ func (s *Server) handleCreateUser(w http.ResponseWriter, r *http.Request) { // at or above their own level. sess := sessionFromCtx(r.Context()) if roleLevel(req.Role) >= roleLevel(sess.Role) { + s.audlog.Log(audit.Entry{ + EventType: audit.EventUserMgmt, + Username: sess.Username, + TenantID: sess.TenantID, + IPAddress: s.remoteIP(r), + Detail: fmt.Sprintf("denied: insufficient privileges to assign role %q on create", req.Role), + Success: false, + }) writeError(w, http.StatusForbidden, "insufficient privileges to assign this role") return } @@ -148,6 +156,14 @@ func (s *Server) handleUpdateUser(w http.ResponseWriter, r *http.Request) { } if sess.TenantID != nil { if target.TenantID == nil || *target.TenantID != *sess.TenantID { + s.audlog.Log(audit.Entry{ + EventType: audit.EventUserMgmt, + Username: sess.Username, + TenantID: sess.TenantID, + IPAddress: s.remoteIP(r), + Detail: fmt.Sprintf("denied: cross-tenant access to user %d", id), + Success: false, + }) writeError(w, http.StatusForbidden, "access denied") return } @@ -159,10 +175,26 @@ func (s *Server) handleUpdateUser(w http.ResponseWriter, r *http.Request) { // checks would otherwise block superadmin-vs-superadmin management. if sess.Role != userstore.RoleSuperAdmin { if roleLevel(target.Role) >= roleLevel(sess.Role) { + s.audlog.Log(audit.Entry{ + EventType: audit.EventUserMgmt, + Username: sess.Username, + TenantID: sess.TenantID, + IPAddress: s.remoteIP(r), + Detail: fmt.Sprintf("denied: insufficient privileges to modify user %d", id), + Success: false, + }) writeError(w, http.StatusForbidden, "insufficient privileges to modify this user") return } if req.Role != nil && roleLevel(*req.Role) >= roleLevel(sess.Role) { + s.audlog.Log(audit.Entry{ + EventType: audit.EventUserMgmt, + Username: sess.Username, + TenantID: sess.TenantID, + IPAddress: s.remoteIP(r), + Detail: fmt.Sprintf("denied: insufficient privileges to assign role %q to user %d", *req.Role, id), + Success: false, + }) writeError(w, http.StatusForbidden, "insufficient privileges to assign this role") return } diff --git a/src/data/features.ts b/src/data/features.ts index 7ba11eb..974d057 100644 --- a/src/data/features.ts +++ b/src/data/features.ts @@ -82,4 +82,5 @@ export const features: Feature[] = [ { id: "PROJ-65", name: "Physische Tenant-Trennung im Storage-Layer", status: "Deployed", frontend: false, backend: true, lastUpdated: "2026-07-04", version: "1.0" }, { id: "PROJ-66", name: "Backup-Strategie für Store, Keyfile, PostgreSQL", status: "Deployed", frontend: false, backend: true, lastUpdated: "2026-07-04", version: "1.0" }, { id: "PROJ-67", name: "Manticore Search Upgrade + Auto-Upgrade-Pfad", status: "Deployed", frontend: false, backend: true, lastUpdated: "2026-07-05", version: "1.0" }, + { id: "PROJ-72", name: "Fix Superadmin kann Passwort/Rolle von Superadmin-Peers nicht ändern", status: "Deployed", frontend: false, backend: true, lastUpdated: "2026-07-27", version: "1.0" }, ];