Files
llmgw/internal/store/forget.go
T

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)
}