121 lines
5.0 KiB
Go
121 lines
5.0 KiB
Go
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
|
|
}
|