fix(PROJ-55): Tenant-Isolation für Rolle "auditor" + Audit-Log korrigieren
Kritischer Sicherheitsbug: Auditoren mit zugewiesenem Tenant sahen Mails und Audit-Log-Einträge anderer Tenants (DSGVO-relevant). auditor wird jetzt analog zu domain_auditor pro Tenant gescoped, sofern tenant_id gesetzt ist (Abwärtskompatibilität: ohne tenant_id bleibt der bisherige globale Zugriff erhalten). Betrifft Mail-Suche, Mail-Detailzugriff, Export, eDiscovery, Threads, OCR sowie das Audit-Log (DB + tamper-evidentes Flat-File), inkl. Befüllung von tenant_id an allen Audit-Log-Schreibstellen.
This commit is contained in:
@@ -71,10 +71,14 @@ func (s *Server) handleExportEDiscovery(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
}
|
||||
|
||||
// Choose index (per-tenant or global)
|
||||
// Choose index (per-tenant or global). PROJ-55: a tenant-scoped auditor
|
||||
// (tenant_id set) uses the per-tenant index like domain_auditor; only a
|
||||
// global auditor (no tenant_id) stays on the global index.
|
||||
searchIdx := s.idx
|
||||
if s.idxMgr != nil && tenantID != nil && sess.Role != userstore.RoleAuditor {
|
||||
usedTenantIndex := false
|
||||
if s.idxMgr != nil && tenantID != nil && !auditorIsGlobal(sess) {
|
||||
searchIdx = s.idxMgr.ForTenant(tenantID)
|
||||
usedTenantIndex = true
|
||||
}
|
||||
|
||||
result, err := searchIdx.Search(searchReq)
|
||||
@@ -83,9 +87,35 @@ func (s *Server) handleExportEDiscovery(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
// Auditor: restrict to no-tenant mails
|
||||
// BUG-1 (PROJ-55): fail-closed tenant post-filter when we fell back to the
|
||||
// global index but the session is tenant-scoped (idxMgr not wired). Without
|
||||
// this a tenant-scoped auditor/domain_auditor would otherwise receive mails
|
||||
// of ALL tenants via eDiscovery. Mirrors the GetAllIDsByTenant fallback in
|
||||
// handleSearch (search_handlers.go).
|
||||
if tenantID != nil && !usedTenantIndex && len(result.Hits) > 0 && !auditorIsGlobal(sess) {
|
||||
allowedIDs, idErr := s.store.GetAllIDsByTenant(r.Context(), tenantID)
|
||||
if idErr != nil {
|
||||
writeError(w, http.StatusInternalServerError, "access check failed")
|
||||
return
|
||||
}
|
||||
allowed := make(map[string]struct{}, len(allowedIDs))
|
||||
for _, id := range allowedIDs {
|
||||
allowed[id] = struct{}{}
|
||||
}
|
||||
filtered := result.Hits[:0]
|
||||
for _, h := range result.Hits {
|
||||
if _, ok := allowed[h.ID]; ok {
|
||||
filtered = append(filtered, h)
|
||||
}
|
||||
}
|
||||
result.Hits = filtered
|
||||
result.Total = len(filtered)
|
||||
}
|
||||
|
||||
// Global auditor (no tenant_id): restrict to no-tenant mails. A tenant-scoped
|
||||
// auditor is already constrained by the per-tenant index above (PROJ-55).
|
||||
var auditorAllowed map[string]struct{}
|
||||
if sess.Role == userstore.RoleAuditor {
|
||||
if auditorIsGlobal(sess) {
|
||||
ids, err := s.store.GetAllIDsWithoutTenant(r.Context())
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "access check failed")
|
||||
@@ -276,6 +306,7 @@ func (s *Server) handleExportEDiscovery(w http.ResponseWriter, r *http.Request)
|
||||
s.audlog.Log(audit.Entry{
|
||||
EventType: audit.EventExport,
|
||||
Username: sess.Username,
|
||||
TenantID: sess.TenantID,
|
||||
IPAddress: s.remoteIP(r),
|
||||
Detail: fmt.Sprintf("ediscovery: case=%q mails=%d", caseName, exported),
|
||||
Success: true,
|
||||
|
||||
Reference in New Issue
Block a user