// Package dsgvorequest implementiert CMP-07: den Workflow für einen // DSGVO-Löschantrag (Art. 17). Ruft AUSSCHLIESSLICH bereits fertige // Bausteine auf — CMP-02 (Suche), RET-03/deletionworkflow // (Aufbewahrungsprüfung, Legal Hold), CMP-06/deletionapproval // (Vier-Augen-Freigabe) — und baut KEINE zweite Prüf-/Freigabelogik // (Ticket-Vorgabe "Bekannte Fehler vermeiden": genau das hatte das // Vorgängerprojekt archivmail falsch gemacht). package dsgvorequest import ( "context" "fmt" "github.com/jackc/pgx/v5/pgxpool" "gitea.perlbach24.de/scripte/nexarch/archive/internal/deletionapproval" "gitea.perlbach24.de/scripte/nexarch/archive/internal/deletionworkflow" "gitea.perlbach24.de/scripte/nexarch/archive/internal/dpreport" ) const ( OutcomeDeletionRequested = "deletion_requested" OutcomeRejected = "rejected" OutcomeAlreadyDeleted = "already_deleted" ) // Decision ist das Ergebnis EINER Einzelentscheidung (Akzeptanzkriterium // 2: niemals eine pauschale Sammelentscheidung). type Decision struct { RetentionObjectID string ObjectType string ObjectReference string Outcome string Reason string // DeletionRequestToken ist NUR bei Outcome==OutcomeDeletionRequested // gesetzt — das einmalige Klartext-Token für die zweite Bestätigung // (CMP-06). Wird NICHT protokolliert (siehe CMP-06: nur der Hash // landet in der DB). DeletionRequestToken string } // ProcessDeletionRequest ist der DSGVO-Löschantrag-Workflow // (Akzeptanzkriterium 1). Findet über CMP-02 alle Objekte der // betroffenen Person (mandantenscharf durch den übergebenen // Tenant-Pool, Akzeptanzkriterium/Pflichtprüfung 3), entscheidet JEDES // Objekt EINZELN: aktive Aufbewahrungspflicht (Frist nicht abgelaufen // ODER Legal Hold) → Ablehnung mit Begründung; sonst → Löschung wird // über CMP-06 angestoßen (nicht sofort ausgeführt — die zweite, // unabhängige Bestätigung bleibt erforderlich). Jede Entscheidung wird // dauerhaft protokolliert (Akzeptanzkriterium 4). func ProcessDeletionRequest(ctx context.Context, pool *pgxpool.Pool, dataSubjectRef, requestedBy string) ([]Decision, error) { records, err := dpreport.SubjectReport(ctx, pool, dataSubjectRef) if err != nil { return nil, fmt.Errorf("dsgvorequest: objekte der betroffenen person ermitteln: %w", err) } decisions := make([]Decision, 0, len(records)) for _, rec := range records { d := Decision{RetentionObjectID: rec.RetentionObjectID, ObjectType: rec.ObjectType, ObjectReference: rec.ObjectReference} if rec.Status == "deleted" { d.Outcome = OutcomeAlreadyDeleted d.Reason = "Objekt wurde bereits zuvor vernichtet" } else { onHold, err := deletionworkflow.IsOnLegalHold(ctx, pool, rec.RetentionObjectID) if err != nil { return nil, fmt.Errorf("dsgvorequest: sperrstatus prüfen (%s): %w", rec.ObjectReference, err) } switch { case onHold: d.Outcome = OutcomeRejected d.Reason = "Aufbewahrungssperre (Legal Hold) aktiv" case rec.Status == "active": d.Outcome = OutcomeRejected d.Reason = "Aufbewahrungsfrist ist noch nicht abgelaufen" default: // "expired", keine Sperre: keine aktive Aufbewahrungspflicht token, err := deletionapproval.RequestDeletion(ctx, pool, rec.RetentionObjectID, requestedBy) if err != nil { return nil, fmt.Errorf("dsgvorequest: löschung anstoßen (%s): %w", rec.ObjectReference, err) } d.Outcome = OutcomeDeletionRequested d.Reason = "keine aktive Aufbewahrungspflicht, Löschung über Vier-Augen-Freigabe angestoßen" d.DeletionRequestToken = token } } if _, err := pool.Exec(ctx, ` INSERT INTO dsgvo_decision_log (data_subject_ref, retention_object_id, object_type, object_reference, outcome, reason, decided_by) VALUES ($1, $2, $3, $4, $5, $6, $7) `, dataSubjectRef, d.RetentionObjectID, d.ObjectType, d.ObjectReference, d.Outcome, d.Reason, requestedBy); err != nil { return nil, fmt.Errorf("dsgvorequest: entscheidung protokollieren (%s): %w", rec.ObjectReference, err) } decisions = append(decisions, d) } return decisions, nil }