@@ -67,6 +67,11 @@ func (e *kvEntry) namespacedKey() string {
6767 return ns
6868}
6969
70+ // privacyPairs is a type alias for a map that holds the privacy pairs, where
71+ // the outer key is the group ID, and the value is a map of real to pseudo
72+ // values.
73+ type privacyPairs = map [int64 ]map [string ]string
74+
7075// MigrateFirewallDBToSQL runs the migration of the firwalldb stores from the
7176// bbolt database to a SQL database. The migration is done in a single
7277// transaction to ensure that all rows in the stores are migrated or none at
@@ -84,10 +89,14 @@ func MigrateFirewallDBToSQL(ctx context.Context, kvStore *bbolt.DB,
8489 return err
8590 }
8691
92+ err = migratePrivacyMapperDBToSQL (ctx , kvStore , sqlTx )
93+ if err != nil {
94+ return err
95+ }
96+
8797 log .Infof ("The rules DB has been migrated from KV to SQL." )
8898
89- // TODO(viktor): Add migration for the privacy mapper and the action
90- // stores.
99+ // TODO(viktor): Add migration for the action stores.
91100
92101 return nil
93102}
@@ -487,3 +496,272 @@ func verifyBktKeys(bkt *bbolt.Bucket, errorOnKeyValues bool,
487496 return fmt .Errorf ("unexpected key found: %s" , key )
488497 })
489498}
499+
500+ func migratePrivacyMapperDBToSQL (ctx context.Context , kvStore * bbolt.DB ,
501+ sqlTx SQLQueries ) error {
502+
503+ log .Infof ("Starting migration of the privacy mapper store to SQL" )
504+
505+ // 1) Collect all privacy pairs from the KV store.
506+ privPairs , err := collectPrivacyPairs (ctx , kvStore , sqlTx )
507+ if err != nil {
508+ return fmt .Errorf ("error migrating privacy mapper store: %w" ,
509+ err )
510+ }
511+
512+ // 2) Insert all collected privacy pairs into the SQL database.
513+ err = insertPrivacyPairs (ctx , sqlTx , privPairs )
514+ if err != nil {
515+ return fmt .Errorf ("insertion of privacy pairs failed: %w" , err )
516+ }
517+
518+ // 3) Validate that all inserted privacy pairs match the original values
519+ // in the KV store. Note that this is done after all values have been
520+ // inserted, to ensure that the migration doesn't overwrite any values
521+ // after they were inserted.
522+ err = validatePrivacyPairsMigration (ctx , sqlTx , privPairs )
523+ if err != nil {
524+ return fmt .Errorf ("migration validation of privacy pairs " +
525+ "failed: %w" , err )
526+ }
527+
528+ log .Infof ("Migration of the privacy mapper stores to SQL completed. " +
529+ "Total number of rows migrated: %d" , len (privPairs ))
530+ return nil
531+ }
532+
533+ // collectPrivacyPairs collects all privacy pairs from the KV store.
534+ func collectPrivacyPairs (ctx context.Context , kvStore * bbolt.DB ,
535+ sqlTx SQLQueries ) (privacyPairs , error ) {
536+
537+ groupPairs := make (privacyPairs )
538+
539+ return groupPairs , kvStore .View (func (kvTx * bbolt.Tx ) error {
540+ bkt := kvTx .Bucket (privacyBucketKey )
541+ if bkt == nil {
542+ // If we haven't generated any privacy bucket yet,
543+ // we can skip the migration, as there are no privacy
544+ // pairs to migrate.
545+ return nil
546+ }
547+
548+ return bkt .ForEach (func (groupId , v []byte ) error {
549+ if v != nil {
550+ return fmt .Errorf ("expected only buckets " +
551+ "under %s bkt, but found value %s" ,
552+ privacyBucketKey , v )
553+ }
554+
555+ gBkt := bkt .Bucket (groupId )
556+ if gBkt == nil {
557+ return fmt .Errorf ("group bkt for group id " +
558+ "%s not found" , groupId )
559+ }
560+
561+ groupSqlId , err := sqlTx .GetSessionIDByAlias (
562+ ctx , groupId ,
563+ )
564+ if errors .Is (err , sql .ErrNoRows ) {
565+ return fmt .Errorf ("session with group id %x " +
566+ "not found in sql db" , groupId )
567+ } else if err != nil {
568+ return err
569+ }
570+
571+ groupRealToPseudoPairs , err := collectGroupPairs (gBkt )
572+ if err != nil {
573+ return fmt .Errorf ("processing group bkt " +
574+ "for group id %s (sqlID %d) failed: %w" ,
575+ groupId , groupSqlId , err )
576+ }
577+
578+ groupPairs [groupSqlId ] = groupRealToPseudoPairs
579+
580+ return nil
581+ })
582+ })
583+ }
584+
585+ // collectGroupPairs collects all privacy pairs for a specific session group,
586+ // i.e. the group buckets under the privacy mapper bucket in the KV store.
587+ // The function returns them as a map, where the key is the real value, and
588+ // the value for the key is the pseudo values.
589+ // It also checks that the pairs are consistent, i.e. that for each real value
590+ // there is a corresponding pseudo value, and vice versa. If the pairs are
591+ // inconsistent, it returns an error indicating the mismatch.
592+ func collectGroupPairs (bkt * bbolt.Bucket ) (map [string ]string , error ) {
593+ var (
594+ realToPseudoRes map [string ]string
595+ pseudoToRealRes map [string ]string
596+ err error
597+ missMatchErr = errors .New ("privacy mapper pairs mismatch" )
598+ )
599+
600+ if realBkt := bkt .Bucket (realToPseudoKey ); realBkt != nil {
601+ realToPseudoRes , err = collectPairs (realBkt )
602+ if err != nil {
603+ return nil , fmt .Errorf ("fetching real to pseudo pairs " +
604+ "failed: %w" , err )
605+ }
606+ } else {
607+ return nil , fmt .Errorf ("%s bucket not found" , realToPseudoKey )
608+ }
609+
610+ if pseudoBkt := bkt .Bucket (pseudoToRealKey ); pseudoBkt != nil {
611+ pseudoToRealRes , err = collectPairs (pseudoBkt )
612+ if err != nil {
613+ return nil , fmt .Errorf ("fetching pseudo to real pairs " +
614+ "failed: %w" , err )
615+ }
616+ } else {
617+ return nil , fmt .Errorf ("%s bucket not found" , pseudoToRealKey )
618+ }
619+
620+ if len (realToPseudoRes ) != len (pseudoToRealRes ) {
621+ return nil , missMatchErr
622+ }
623+
624+ for realVal , pseudoVal := range realToPseudoRes {
625+ if rv , ok := pseudoToRealRes [pseudoVal ]; ! ok || rv != realVal {
626+ return nil , missMatchErr
627+ }
628+ }
629+
630+ return realToPseudoRes , nil
631+ }
632+
633+ // collectPairs collects all privacy pairs from a specific realToPseudoKey or
634+ // pseudoToRealKey bucket in the KV store. It returns a map where the key is
635+ // the real value or pseudo value, and the value is the corresponding pseudo
636+ // value or real value, respectively (depending on if the realToPseudo or
637+ // pseudoToReal bucket is passed to the function).
638+ func collectPairs (pairsBucket * bbolt.Bucket ) (map [string ]string , error ) {
639+ pairsRes := make (map [string ]string )
640+
641+ return pairsRes , pairsBucket .ForEach (func (k , v []byte ) error {
642+ if v == nil {
643+ return fmt .Errorf ("expected only key-values under " +
644+ "pairs bucket, but found bucket %s" , k )
645+ }
646+
647+ if len (v ) == 0 {
648+ return fmt .Errorf ("empty value stored for privacy " +
649+ "pairs key %s" , k )
650+ }
651+
652+ pairsRes [string (k )] = string (v )
653+
654+ return nil
655+ })
656+ }
657+
658+ // insertPrivacyPairs inserts the collected privacy pairs into the SQL database.
659+ func insertPrivacyPairs (ctx context.Context , sqlTx SQLQueries ,
660+ pairs privacyPairs ) error {
661+
662+ for groupId , groupPairs := range pairs {
663+ err := insertGroupPairs (ctx , sqlTx , groupPairs , groupId )
664+ if err != nil {
665+ return fmt .Errorf ("inserting group pairs for group " +
666+ "id %d failed: %w" , groupId , err )
667+ }
668+ }
669+
670+ return nil
671+ }
672+
673+ // insertGroupPairs inserts the privacy pairs for a specific group into
674+ // the SQL database. It checks for duplicates before inserting, and returns
675+ // an error if a duplicate pair is found. The function takes a map of real
676+ // to pseudo values, where the key is the real value and the value is the
677+ // corresponding pseudo value.
678+ func insertGroupPairs (ctx context.Context , sqlTx SQLQueries ,
679+ pairs map [string ]string , groupID int64 ) error {
680+
681+ for realVal , pseudoVal := range pairs {
682+ err := sqlTx .InsertPrivacyPair (
683+ ctx , sqlc.InsertPrivacyPairParams {
684+ GroupID : groupID ,
685+ RealVal : realVal ,
686+ PseudoVal : pseudoVal ,
687+ },
688+ )
689+ if err != nil {
690+ return fmt .Errorf ("inserting privacy pair %s:%s " +
691+ "failed: %w" , realVal , pseudoVal , err )
692+ }
693+ }
694+
695+ return nil
696+ }
697+
698+ // validatePrivacyPairsMigration validates that the migrated privacy pairs
699+ // match the original values in the KV store.
700+ func validatePrivacyPairsMigration (ctx context.Context , sqlTx SQLQueries ,
701+ pairs privacyPairs ) error {
702+
703+ for groupId , groupPairs := range pairs {
704+ err := validateGroupPairsMigration (
705+ ctx , sqlTx , groupPairs , groupId ,
706+ )
707+ if err != nil {
708+ return fmt .Errorf ("migration validation of privacy " +
709+ "pairs for group %d failed: %w" , groupId , err )
710+ }
711+ }
712+
713+ return nil
714+ }
715+
716+ // validateGroupPairsMigration validates that the migrated privacy pairs for
717+ // a specific group match the original values in the KV store. It checks that
718+ // for each real value, the pseudo value in the SQL database matches the
719+ // original pseudo value, and vice versa. If any mismatch is found, it returns
720+ // an error indicating the mismatch.
721+ func validateGroupPairsMigration (ctx context.Context , sqlTx SQLQueries ,
722+ pairs map [string ]string , groupID int64 ) error {
723+
724+ for realVal , pseudoVal := range pairs {
725+ resPseudoVal , err := sqlTx .GetPseudoForReal (
726+ ctx , sqlc.GetPseudoForRealParams {
727+ GroupID : groupID ,
728+ RealVal : realVal ,
729+ },
730+ )
731+ if errors .Is (err , sql .ErrNoRows ) {
732+ return fmt .Errorf ("migrated privacy pair %s:%s not " +
733+ "found for real value" , realVal , pseudoVal )
734+ }
735+ if err != nil {
736+ return err
737+ }
738+
739+ if resPseudoVal != pseudoVal {
740+ return fmt .Errorf ("pseudo value in db %s, does not " +
741+ "match original value %s, for real value %s" ,
742+ resPseudoVal , pseudoVal , realVal )
743+ }
744+
745+ resRealVal , err := sqlTx .GetRealForPseudo (
746+ ctx , sqlc.GetRealForPseudoParams {
747+ GroupID : groupID ,
748+ PseudoVal : pseudoVal ,
749+ },
750+ )
751+ if errors .Is (err , sql .ErrNoRows ) {
752+ return fmt .Errorf ("migrated privacy pair %s:%s not " +
753+ "found for pseudo value" , realVal , pseudoVal )
754+ }
755+ if err != nil {
756+ return err
757+ }
758+
759+ if resRealVal != realVal {
760+ return fmt .Errorf ("real value in db %s, does not " +
761+ "match original value %s, for pseudo value %s" ,
762+ resRealVal , realVal , pseudoVal )
763+ }
764+ }
765+
766+ return nil
767+ }
0 commit comments