Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
109 lines
3.6 KiB
Go
109 lines
3.6 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"time"
|
|
)
|
|
|
|
// Forgotten is what forgetting a person did, for saying so.
|
|
type Forgotten struct {
|
|
// Pseudonym is what their usage is counted under now: random, so it
|
|
// links to nothing.
|
|
Pseudonym string
|
|
// UsageRows and RollupRows are the request records and sums moved to it.
|
|
UsageRows int64
|
|
RollupRows int64
|
|
// Requests, Files and Batches are kept requests, and personal files and
|
|
// batches, deleted; AuditEvents the audit lines their name was taken out of.
|
|
Requests int64
|
|
Files int64
|
|
Batches int64
|
|
AuditEvents int64
|
|
}
|
|
|
|
// ForgetUser erases a person: what was kept of their requests, their
|
|
// personal files and batches and their limits are deleted; their usage — the
|
|
// request records and every sum of them — is kept, for the totals stay true,
|
|
// under a random pseudonym; their name is taken out of the audit log; and the
|
|
// account goes as DeleteUser removes it. What remains cannot be tied back to
|
|
// them.
|
|
//
|
|
// Conversations and agent notes belong to projects, not people, and are not
|
|
// touched: a project forgets them by its retention, or by
|
|
// DELETE /v1/threads/{id}.
|
|
func (s *DB) ForgetUser(ctx context.Context, userID string, at time.Time) (Forgotten, error) {
|
|
u, err := s.UserByID(ctx, userID)
|
|
if err != nil {
|
|
return Forgotten{}, err
|
|
}
|
|
b := make([]byte, 8)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return Forgotten{}, err
|
|
}
|
|
f := Forgotten{Pseudonym: "forgotten-" + hex.EncodeToString(b)}
|
|
err = s.inTx(ctx, func(c conn) error {
|
|
n := func(q string, args ...any) (int64, error) {
|
|
res, err := c.ExecContext(ctx, q, args...)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return res.RowsAffected()
|
|
}
|
|
var err error
|
|
if f.Requests, err = n(`DELETE FROM request_bodies WHERE request_id IN
|
|
(SELECT request_id FROM usage WHERE user_id = ? AND request_id <> '')`, userID); err != nil {
|
|
return err
|
|
}
|
|
session, pseudoSession := "session:"+userID, "session:"+f.Pseudonym
|
|
if f.UsageRows, err = n(`UPDATE usage SET user_id = ? WHERE user_id = ?`, f.Pseudonym, userID); err != nil {
|
|
return err
|
|
}
|
|
if _, err = n(`UPDATE usage SET key_id = ? WHERE key_id = ?`, pseudoSession, session); err != nil {
|
|
return err
|
|
}
|
|
for _, t := range []string{"usage_5m", "usage_1h", "usage_1d"} {
|
|
m, err := n(`UPDATE `+t+` SET user_id = ? WHERE user_id = ?`, f.Pseudonym, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
f.RollupRows += m
|
|
if _, err := n(`UPDATE `+t+` SET key_id = ? WHERE key_id = ?`, pseudoSession, session); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
owner := "user:" + userID
|
|
if _, err = n(`DELETE FROM batch_results WHERE batch_id IN (SELECT id FROM batches WHERE owner = ?)`, owner); err != nil {
|
|
return err
|
|
}
|
|
if f.Batches, err = n(`DELETE FROM batches WHERE owner = ?`, owner); err != nil {
|
|
return err
|
|
}
|
|
if f.Files, err = n(`DELETE FROM files WHERE owner = ?`, owner); err != nil {
|
|
return err
|
|
}
|
|
if _, err = n(`DELETE FROM limits WHERE scope_type = ? AND scope_id = ?`, string(ScopeUser), userID); err != nil {
|
|
return err
|
|
}
|
|
// Who did what stays in the audit log; that it was them does not.
|
|
for _, name := range []string{u.Email, userID} {
|
|
if name == "" {
|
|
continue
|
|
}
|
|
m, err := n(`UPDATE audit_events SET actor = REPLACE(actor, ?, ?), target = REPLACE(target, ?, ?),
|
|
detail = REPLACE(detail, ?, ?) WHERE actor LIKE ? OR target LIKE ? OR detail LIKE ?`,
|
|
name, f.Pseudonym, name, f.Pseudonym, name, f.Pseudonym, "%"+name+"%", "%"+name+"%", "%"+name+"%")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
f.AuditEvents += m
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return Forgotten{}, err
|
|
}
|
|
return f, s.DeleteUser(ctx, userID, at)
|
|
}
|