Skip to content

Commit 5875147

Browse files
authored
Merge pull request #1092 from ViktorTigerstrom/2025-06-migrate-privacy-mapper
[sql-44] firewalldb: add migration code for privacy mapper from kvdb to SQL
2 parents 55f5682 + 205ec9e commit 5875147

File tree

2 files changed

+573
-34
lines changed

2 files changed

+573
-34
lines changed

firewalldb/sql_migration.go

Lines changed: 289 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,16 @@ 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
7378
// all.
7479
//
75-
// Note that this migration currently only migrates the kvstores, but will be
76-
// extended in the future to also migrate the privacy mapper and action stores.
77-
//
7880
// NOTE: As sessions may contain linked sessions and accounts, the sessions and
7981
// accounts sql migration MUST be run prior to this migration.
8082
func MigrateFirewallDBToSQL(ctx context.Context, kvStore *bbolt.DB,
@@ -87,10 +89,14 @@ func MigrateFirewallDBToSQL(ctx context.Context, kvStore *bbolt.DB,
8789
return err
8890
}
8991

92+
err = migratePrivacyMapperDBToSQL(ctx, kvStore, sqlTx)
93+
if err != nil {
94+
return err
95+
}
96+
9097
log.Infof("The rules DB has been migrated from KV to SQL.")
9198

92-
// TODO(viktor): Add migration for the privacy mapper and the action
93-
// stores.
99+
// TODO(viktor): Add migration for the action stores.
94100

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

0 commit comments

Comments
 (0)