Files
story/internal/privacy/access_processor.go
T

121 lines
5.0 KiB
Go
Raw Normal View History

2026-08-26 06:41:07 +08:00
package privacy
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"data-terminal-cloud/internal/audit"
"data-terminal-cloud/internal/database"
"data-terminal-cloud/internal/outbox"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
type AccessProcessor struct {
pool *pgxpool.Pool
audit *audit.Repository
}
func NewAccessProcessor(pool *pgxpool.Pool) (*AccessProcessor, error) {
if pool == nil {
return nil, errors.New("database pool is required")
}
return &AccessProcessor{pool: pool, audit: audit.NewRepository()}, nil
}
func (p *AccessProcessor) Handle(ctx context.Context, event outbox.Event) error {
return database.Transact(ctx, p.pool, func(tx pgx.Tx) error {
var tenantID, subjectID, publicID, status, requestType string
var scopeJSON []byte
if err := tx.QueryRow(ctx, `SELECT tenant_id::text,subject_id::text,public_id::text,status,request_type,scope FROM privacy_request WHERE id=$1::uuid FOR UPDATE`, event.AggregateID).Scan(&tenantID, &subjectID, &publicID, &status, &requestType, &scopeJSON); errors.Is(err, pgx.ErrNoRows) {
return nil
} else if err != nil {
return fmt.Errorf("load privacy access request: %w", err)
}
if status == "COMPLETED" {
return nil
}
if requestType != "ACCESS" {
return fmt.Errorf("privacy request type %s is not handled by access processor", requestType)
}
if _, err := tx.Exec(ctx, `UPDATE privacy_request SET status='VERIFYING',version=version+1,updated_at=now() WHERE id=$1::uuid AND status='SUBMITTED'`, event.AggregateID); err != nil {
return err
}
if _, err := tx.Exec(ctx, `UPDATE privacy_request SET status='EXECUTING',version=version+1,updated_at=now() WHERE id=$1::uuid AND status IN ('VERIFYING','PARTIAL_FAILED')`, event.AggregateID); err != nil {
return err
}
if _, err := tx.Exec(ctx, `UPDATE privacy_subtask SET status='RUNNING',attempts=attempts+1,last_error=NULL,updated_at=now() WHERE privacy_request_id=$1::uuid AND subsystem='postgresql' AND target_type='subject_data_inventory'`, event.AggregateID); err != nil {
return err
}
counts, err := collectInventory(ctx, tx, tenantID, subjectID)
if err != nil {
return err
}
var scope map[string]any
if err := json.Unmarshal(scopeJSON, &scope); err != nil {
return fmt.Errorf("decode privacy scope: %w", err)
}
result := map[string]any{
"inventory_counts": filterInventory(counts, scope),
"generated_at": time.Now().UTC().Format(time.RFC3339Nano),
"content_included": false,
}
resultJSON, err := json.Marshal(result)
if err != nil {
return err
}
if _, err := tx.Exec(ctx, `UPDATE privacy_subtask SET status='COMPLETED',completed_at=now(),updated_at=now() WHERE privacy_request_id=$1::uuid AND subsystem='postgresql' AND target_type='subject_data_inventory'`, event.AggregateID); err != nil {
return err
}
if _, err := tx.Exec(ctx, `UPDATE privacy_request SET status='COMPLETED',result_summary=$2::jsonb,completed_at=now(),version=version+1,updated_at=now() WHERE id=$1::uuid`, event.AggregateID, resultJSON); err != nil {
return err
}
return p.audit.Record(ctx, tx, audit.Event{TenantID: &tenantID, ActorType: "system", Action: "privacy.access.complete", ResourceType: "privacy_request", ResourceID: &publicID, Result: "success", Detail: map[string]any{"subject_digest_scope": "tenant_subject", "content_included": false}})
})
}
func collectInventory(ctx context.Context, tx pgx.Tx, tenantID, subjectID string) (map[string]int64, error) {
queries := map[string]string{
"profile": `SELECT count(*) FROM subject_profile WHERE tenant_id=$1::uuid AND id=$2::uuid`,
"consents": `SELECT count(*) FROM consent_record WHERE tenant_id=$1::uuid AND subject_id=$2::uuid`,
"device_bindings": `SELECT count(*) FROM device_binding WHERE tenant_id=$1::uuid AND subject_id=$2::uuid`,
"conversations": `SELECT count(*) FROM conversation WHERE tenant_id=$1::uuid AND subject_id=$2::uuid`,
"transcripts": `SELECT count(*) FROM conversation_turn WHERE tenant_id=$1::uuid AND subject_id=$2::uuid`,
"summaries": `SELECT count(*) FROM conversation_summary WHERE tenant_id=$1::uuid AND subject_id=$2::uuid`,
"stories": `SELECT count(*) FROM story_draft WHERE tenant_id=$1::uuid AND subject_id=$2::uuid`,
"memories": `SELECT count(*) FROM curated_memory WHERE tenant_id=$1::uuid AND subject_id=$2::uuid`,
}
result := make(map[string]int64, len(queries))
for category, query := range queries {
var count int64
if err := tx.QueryRow(ctx, query, tenantID, subjectID).Scan(&count); err != nil {
return nil, fmt.Errorf("count privacy inventory category %s: %w", category, err)
}
result[category] = count
}
return result, nil
}
func filterInventory(counts map[string]int64, scope map[string]any) map[string]int64 {
requested, ok := scope["categories"].([]any)
if !ok || len(requested) == 0 {
return counts
}
result := make(map[string]int64, len(requested))
for _, value := range requested {
category, ok := value.(string)
if !ok {
continue
}
if count, exists := counts[category]; exists {
result[category] = count
}
}
return result
}