docs(PROJ-72): Feature-Spec nachtragen + Audit-Log-Härtung für abgelehnte Privilege-Checks
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.
This commit is contained in:
+2
-1
@@ -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-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-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-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 |
|
||||||
|
|
||||||
<!-- Add features above this line -->
|
<!-- Add features above this line -->
|
||||||
|
|
||||||
## Next Available ID: PROJ-72
|
## Next Available ID: PROJ-73
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -66,6 +66,14 @@ func (s *Server) handleCreateUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
// at or above their own level.
|
// at or above their own level.
|
||||||
sess := sessionFromCtx(r.Context())
|
sess := sessionFromCtx(r.Context())
|
||||||
if roleLevel(req.Role) >= roleLevel(sess.Role) {
|
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")
|
writeError(w, http.StatusForbidden, "insufficient privileges to assign this role")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -148,6 +156,14 @@ func (s *Server) handleUpdateUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
if sess.TenantID != nil {
|
if sess.TenantID != nil {
|
||||||
if target.TenantID == nil || *target.TenantID != *sess.TenantID {
|
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")
|
writeError(w, http.StatusForbidden, "access denied")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -159,10 +175,26 @@ func (s *Server) handleUpdateUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
// checks would otherwise block superadmin-vs-superadmin management.
|
// checks would otherwise block superadmin-vs-superadmin management.
|
||||||
if sess.Role != userstore.RoleSuperAdmin {
|
if sess.Role != userstore.RoleSuperAdmin {
|
||||||
if roleLevel(target.Role) >= roleLevel(sess.Role) {
|
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")
|
writeError(w, http.StatusForbidden, "insufficient privileges to modify this user")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if req.Role != nil && roleLevel(*req.Role) >= roleLevel(sess.Role) {
|
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")
|
writeError(w, http.StatusForbidden, "insufficient privileges to assign this role")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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-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-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-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" },
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user