chore: establish local platform baseline
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user