feat(PROJ-65): Physische Tenant-Trennung im Storage-Layer (Hardlink-Ordner)
Jeder Tenant bekommt ein eigenes Verzeichnis store/tenant_<id>/, das per Hardlink auf die kanonische content-adressierte Datei zeigt — das bestehende Cross-Tenant-Dedup-Modell (email_refs M:N, PROJ-32/37) bleibt dadurch erhalten, kein Speicherplatz-Mehrverbrauch. Neues CLI-Subcommand `archivmail migrate-tenant-dirs` zieht Bestandsdaten einmalig nach (idempotent). Zusätzlich neuer Status-Check checkStoragePermissions (warnt bei zu offenen store_path-Rechten, analog checkEncryption/PROJ-49). DB-gestützte Zugriffskontrolle bleibt der maßgebliche Zugriffspfad im Code; die Tenant-Ordner sind eine zusätzliche Defense-in-Depth-Ebene für manuelle Dateisystem-Audits. Kein lokaler go build möglich, QA folgt auf Testserver.
This commit is contained in:
@@ -436,6 +436,7 @@ func (s *Store) Save(ctx context.Context, raw []byte, _ time.Time, tenantID *int
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (email_id, tenant_id) DO NOTHING
|
||||
`, existingID, *tenantID)
|
||||
s.linkTenantDir(existingID, *tenantID)
|
||||
}
|
||||
return existingID, nil
|
||||
}
|
||||
@@ -518,6 +519,7 @@ func (s *Store) Save(ctx context.Context, raw []byte, _ time.Time, tenantID *int
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (email_id, tenant_id) DO NOTHING
|
||||
`, conflictID, *tenantID)
|
||||
s.linkTenantDir(conflictID, *tenantID)
|
||||
}
|
||||
return conflictID, nil
|
||||
}
|
||||
@@ -548,6 +550,9 @@ func (s *Store) Save(ctx context.Context, raw []byte, _ time.Time, tenantID *int
|
||||
ON CONFLICT (email_id, tenant_id) DO NOTHING
|
||||
`, id, *tenantID)
|
||||
}
|
||||
if tenantID != nil {
|
||||
s.linkTenantDir(id, *tenantID)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
@@ -737,6 +742,11 @@ func (s *Store) Load(id string) ([]byte, error) {
|
||||
func (s *Store) Delete(id string) error {
|
||||
ctx := context.Background()
|
||||
|
||||
// Captured before the DB rows are deleted below (PROJ-65): once emails/
|
||||
// email_refs are gone we can no longer ask which tenant directories held
|
||||
// a hardlink to this mail.
|
||||
var tenantIDs []int64
|
||||
|
||||
if s.db != nil {
|
||||
var until *time.Time
|
||||
_ = s.db.QueryRow(ctx, `SELECT retain_until FROM emails WHERE id=$1`, id).Scan(&until)
|
||||
@@ -744,6 +754,8 @@ func (s *Store) Delete(id string) error {
|
||||
return ErrRetentionLock
|
||||
}
|
||||
|
||||
tenantIDs, _ = s.TenantsForMail(ctx, id)
|
||||
|
||||
tx, err := s.db.Begin(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("storage: delete: begin tx: %w", err)
|
||||
@@ -774,6 +786,8 @@ func (s *Store) Delete(id string) error {
|
||||
return fmt.Errorf("storage: delete: file: %w", err)
|
||||
}
|
||||
|
||||
s.unlinkTenantDirs(id, tenantIDs)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user