@@ -14,6 +14,7 @@ import (
1414 "github.com/jackc/pgx/v5/pgtype"
1515 "go.temporal.io/sdk/log"
1616 "google.golang.org/protobuf/encoding/protojson"
17+ "google.golang.org/protobuf/proto"
1718
1819 "github.com/PeerDB-io/peerdb/flow/alerting"
1920 "github.com/PeerDB-io/peerdb/flow/connectors/utils/monitoring"
@@ -24,8 +25,9 @@ import (
2425)
2526
2627const (
27- lastSyncStateTableName = "metadata_last_sync_state"
28- qrepTableName = "metadata_qrep_partitions"
28+ lastSyncStateTableName = "metadata_last_sync_state"
29+ qrepTableName = "metadata_qrep_partitions"
30+ qrepPartitionRangesTableName = "metadata_qrep_partition_ranges"
2931)
3032
3133type PostgresMetadata struct {
@@ -286,6 +288,146 @@ func (p *PostgresMetadata) IsQRepPartitionSynced(ctx context.Context, req *proto
286288 return exists , nil
287289}
288290
291+ // OffloadSensitivePartitionRanges encrypts sensitive partition ranges, persists them to
292+ // the catalog, and replaces each partition's range in-place with an empty range.
293+ // Used in conjunction with RestoreSensitivePartitionRanges.
294+ func OffloadSensitivePartitionRanges (
295+ ctx context.Context ,
296+ pool shared.CatalogPool ,
297+ parentMirrorName string ,
298+ runUUID string ,
299+ partitions []* protos.QRepPartition ,
300+ ) error {
301+ if ! partitionsContainSensitiveRanges (partitions ) {
302+ return nil
303+ }
304+
305+ key , err := internal .PeerDBCurrentEncKey (ctx )
306+ if err != nil {
307+ return fmt .Errorf ("failed to load current encryption key: %w" , err )
308+ }
309+
310+ tx , err := pool .Begin (ctx )
311+ if err != nil {
312+ return fmt .Errorf ("failed to begin transaction: %w" , err )
313+ }
314+ defer shared .RollbackTx (tx , internal .LoggerFromCtx (ctx ))
315+
316+ // delete any orphaned rows from previously failed attempt
317+ if _ , err := tx .Exec (ctx ,
318+ `DELETE FROM ` + qrepPartitionRangesTableName + ` WHERE run_uuid=$1` , runUUID ,
319+ ); err != nil {
320+ return fmt .Errorf ("failed to clear existing partition ranges: %w" , err )
321+ }
322+
323+ for _ , partition := range partitions {
324+ payload , err := proto .Marshal (partition .Range )
325+ if err != nil {
326+ return fmt .Errorf ("failed to marshal partition range: %w" , err )
327+ }
328+ encrypted , err := key .Encrypt (payload )
329+ if err != nil {
330+ return fmt .Errorf ("failed to encrypt partition range: %w" , err )
331+ }
332+
333+ if _ , err := tx .Exec (ctx ,
334+ `INSERT INTO ` + qrepPartitionRangesTableName +
335+ `(parent_mirror_name,run_uuid,partition_uuid,enc_key_id,range_payload) VALUES($1,$2,$3,$4,$5)` ,
336+ parentMirrorName , runUUID , partition .PartitionId , key .ID , encrypted ,
337+ ); err != nil {
338+ return fmt .Errorf ("failed to persist encrypted partition range: %w" , err )
339+ }
340+
341+ partition .Range = & protos.PartitionRange {
342+ Range : & protos.PartitionRange_StringRange {StringRange : & protos.StringPartitionRange {}},
343+ }
344+ }
345+
346+ if err := tx .Commit (ctx ); err != nil {
347+ return fmt .Errorf ("failed to commit partition ranges: %w" , err )
348+ }
349+ return nil
350+ }
351+
352+ // RestoreSensitivePartitionRanges hydrates sensitive partitions in-place with the decrypted
353+ // values fetched from the catalog. Used in conjunction with OffloadSensitivePartitionRanges
354+ func RestoreSensitivePartitionRanges (
355+ ctx context.Context ,
356+ pool shared.CatalogPool ,
357+ runUUID string ,
358+ partitions []* protos.QRepPartition ,
359+ ) error {
360+ if ! partitionsContainSensitiveRanges (partitions ) {
361+ return nil
362+ }
363+
364+ partitionIDs := make ([]string , 0 , len (partitions ))
365+ partitionByIDs := make (map [string ]* protos.QRepPartition , len (partitions ))
366+ for _ , partition := range partitions {
367+ partitionIDs = append (partitionIDs , partition .PartitionId )
368+ partitionByIDs [partition .PartitionId ] = partition
369+ }
370+
371+ rows , err := pool .Query (ctx ,
372+ `SELECT partition_uuid,enc_key_id,range_payload FROM ` + qrepPartitionRangesTableName + ` ` +
373+ `WHERE run_uuid=$1 AND partition_uuid=ANY($2)` ,
374+ runUUID , partitionIDs ,
375+ )
376+ if err != nil {
377+ return fmt .Errorf ("failed to query partition ranges: %w" , err )
378+ }
379+ results , err := pgx .CollectRows (rows , pgx.RowToStructByPos [struct {
380+ PartitionID string
381+ EncKeyID string
382+ Payload []byte
383+ }])
384+ if err != nil {
385+ return fmt .Errorf ("failed to read encrypted partition ranges: %w" , err )
386+ }
387+
388+ for _ , res := range results {
389+ partition , ok := partitionByIDs [res .PartitionID ]
390+ if ! ok {
391+ return fmt .Errorf ("received unexpected partition range for partition %s" , res .PartitionID )
392+ }
393+
394+ payload , err := internal .Decrypt (ctx , res .EncKeyID , res .Payload )
395+ if err != nil {
396+ return fmt .Errorf ("failed to decrypt partition range for %s: %w" , res .PartitionID , err )
397+ }
398+
399+ var partitionRange protos.PartitionRange
400+ if err := proto .Unmarshal (payload , & partitionRange ); err != nil {
401+ return fmt .Errorf ("failed to unmarshal partition range for %s: %w" , res .PartitionID , err )
402+ }
403+ partition .Range = & partitionRange
404+ delete (partitionByIDs , res .PartitionID )
405+ }
406+
407+ if len (partitionByIDs ) > 0 {
408+ missing := make ([]string , 0 , len (partitionByIDs ))
409+ for id := range partitionByIDs {
410+ missing = append (missing , id )
411+ }
412+ return fmt .Errorf ("missing encrypted partition ranges for partitions: %v" , missing )
413+ }
414+
415+ return nil
416+ }
417+
418+ func partitionsContainSensitiveRanges (partitions []* protos.QRepPartition ) bool {
419+ for _ , partition := range partitions {
420+ if partition == nil || partition .Range == nil {
421+ return false
422+ }
423+ // String ranges may contain PII data like email, so deem it as sensitive.
424+ if _ , ok := partition .Range .Range .(* protos.PartitionRange_StringRange ); ok {
425+ return true
426+ }
427+ }
428+ return false
429+ }
430+
289431func (p * PostgresMetadata ) SyncFlowCleanup (ctx context.Context , jobName string ) error {
290432 tx , err := p .pool .Begin (ctx )
291433 if err != nil {
@@ -313,5 +455,9 @@ func SyncFlowCleanupInTx(ctx context.Context, tx pgx.Tx, jobName string) error {
313455 return err
314456 }
315457
458+ if _ , err := tx .Exec (ctx , `DELETE FROM ` + qrepPartitionRangesTableName + ` WHERE parent_mirror_name = $1` , jobName ); err != nil {
459+ return err
460+ }
461+
316462 return nil
317463}
0 commit comments