mirror of
https://github.com/netbirdio/netbird.git
synced 2025-06-20 01:38:41 +02:00
[management] Migrate events sqlite store to gorm (#3837)
This commit is contained in:
parent
1d4cfb83e7
commit
4785f23fc4
2
go.mod
2
go.mod
@ -59,7 +59,6 @@ require (
|
|||||||
github.com/hashicorp/go-version v1.6.0
|
github.com/hashicorp/go-version v1.6.0
|
||||||
github.com/libdns/route53 v1.5.0
|
github.com/libdns/route53 v1.5.0
|
||||||
github.com/libp2p/go-netroute v0.2.1
|
github.com/libp2p/go-netroute v0.2.1
|
||||||
github.com/mattn/go-sqlite3 v1.14.22
|
|
||||||
github.com/mdlayher/socket v0.5.1
|
github.com/mdlayher/socket v0.5.1
|
||||||
github.com/miekg/dns v1.1.59
|
github.com/miekg/dns v1.1.59
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2
|
github.com/mitchellh/hashstructure/v2 v2.0.2
|
||||||
@ -195,6 +194,7 @@ require (
|
|||||||
github.com/libdns/libdns v0.2.2 // indirect
|
github.com/libdns/libdns v0.2.2 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae // indirect
|
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae // indirect
|
||||||
github.com/magiconair/properties v1.8.7 // indirect
|
github.com/magiconair/properties v1.8.7 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||||
github.com/mdlayher/genetlink v1.3.2 // indirect
|
github.com/mdlayher/genetlink v1.3.2 // indirect
|
||||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect
|
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect
|
||||||
github.com/mholt/acmez/v2 v2.0.1 // indirect
|
github.com/mholt/acmez/v2 v2.0.1 // indirect
|
||||||
|
@ -19,22 +19,22 @@ type Event struct {
|
|||||||
// Timestamp of the event
|
// Timestamp of the event
|
||||||
Timestamp time.Time
|
Timestamp time.Time
|
||||||
// Activity that was performed during the event
|
// Activity that was performed during the event
|
||||||
Activity ActivityDescriber
|
Activity Activity `gorm:"type:integer"`
|
||||||
// ID of the event (can be empty, meaning that it wasn't yet generated)
|
// ID of the event (can be empty, meaning that it wasn't yet generated)
|
||||||
ID uint64
|
ID uint64 `gorm:"primaryKey;autoIncrement"`
|
||||||
// InitiatorID is the ID of an object that initiated the event (e.g., a user)
|
// InitiatorID is the ID of an object that initiated the event (e.g., a user)
|
||||||
InitiatorID string
|
InitiatorID string
|
||||||
// InitiatorName is the name of an object that initiated the event.
|
// InitiatorName is the name of an object that initiated the event.
|
||||||
InitiatorName string
|
InitiatorName string `gorm:"-"`
|
||||||
// InitiatorEmail is the email address of an object that initiated the event.
|
// InitiatorEmail is the email address of an object that initiated the event.
|
||||||
InitiatorEmail string
|
InitiatorEmail string `gorm:"-"`
|
||||||
// TargetID is the ID of an object that was effected by the event (e.g., a peer)
|
// TargetID is the ID of an object that was effected by the event (e.g., a peer)
|
||||||
TargetID string
|
TargetID string
|
||||||
// AccountID is the ID of an account where the event happened
|
// AccountID is the ID of an account where the event happened
|
||||||
AccountID string
|
AccountID string
|
||||||
|
|
||||||
// Meta of the event, e.g. deleted peer information like name, IP, etc
|
// Meta of the event, e.g. deleted peer information like name, IP, etc
|
||||||
Meta map[string]any
|
Meta map[string]any `gorm:"serializer:json"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy the event
|
// Copy the event
|
||||||
@ -57,3 +57,10 @@ func (e *Event) Copy() *Event {
|
|||||||
Meta: meta,
|
Meta: meta,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DeletedUser struct {
|
||||||
|
ID string `gorm:"primaryKey"`
|
||||||
|
Email string `gorm:"not null"`
|
||||||
|
Name string
|
||||||
|
EncAlgo string `gorm:"not null"`
|
||||||
|
}
|
||||||
|
@ -2,156 +2,180 @@ package sqlite
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/management/server/activity"
|
||||||
|
"github.com/netbirdio/netbird/management/server/migration"
|
||||||
)
|
)
|
||||||
|
|
||||||
func migrate(ctx context.Context, crypt *FieldEncrypt, db *sql.DB) error {
|
func migrate(ctx context.Context, crypt *FieldEncrypt, db *gorm.DB) error {
|
||||||
if _, err := db.Exec(createTableQuery); err != nil {
|
migrations := getMigrations(ctx, crypt)
|
||||||
|
|
||||||
|
for _, m := range migrations {
|
||||||
|
if err := m(db); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := db.Exec(creatTableDeletedUsersQuery); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := updateDeletedUsersTable(ctx, db); err != nil {
|
|
||||||
return fmt.Errorf("failed to update deleted_users table: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return migrateLegacyEncryptedUsersToGCM(ctx, crypt, db)
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateDeletedUsersTable checks and updates the deleted_users table schema to ensure required columns exist.
|
|
||||||
func updateDeletedUsersTable(ctx context.Context, db *sql.DB) error {
|
|
||||||
exists, err := checkColumnExists(db, "deleted_users", "name")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
log.WithContext(ctx).Debug("Adding name column to the deleted_users table")
|
|
||||||
|
|
||||||
_, err = db.Exec(`ALTER TABLE deleted_users ADD COLUMN name TEXT;`)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.WithContext(ctx).Debug("Successfully added name column to the deleted_users table")
|
|
||||||
}
|
|
||||||
|
|
||||||
exists, err = checkColumnExists(db, "deleted_users", "enc_algo")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
log.WithContext(ctx).Debug("Adding enc_algo column to the deleted_users table")
|
|
||||||
|
|
||||||
_, err = db.Exec(`ALTER TABLE deleted_users ADD COLUMN enc_algo TEXT;`)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.WithContext(ctx).Debug("Successfully added enc_algo column to the deleted_users table")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// migrateLegacyEncryptedUsersToGCM migrates previously encrypted data using,
|
type migrationFunc func(*gorm.DB) error
|
||||||
|
|
||||||
|
func getMigrations(ctx context.Context, crypt *FieldEncrypt) []migrationFunc {
|
||||||
|
return []migrationFunc{
|
||||||
|
func(db *gorm.DB) error {
|
||||||
|
return migration.MigrateNewField[activity.DeletedUser](ctx, db, "name", "")
|
||||||
|
},
|
||||||
|
func(db *gorm.DB) error {
|
||||||
|
return migration.MigrateNewField[activity.DeletedUser](ctx, db, "enc_algo", "")
|
||||||
|
},
|
||||||
|
func(db *gorm.DB) error {
|
||||||
|
return migrateLegacyEncryptedUsersToGCM(ctx, db, crypt)
|
||||||
|
},
|
||||||
|
func(db *gorm.DB) error {
|
||||||
|
return migrateDuplicateDeletedUsers(ctx, db)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// migrateLegacyEncryptedUsersToGCM migrates previously encrypted data using
|
||||||
// legacy CBC encryption with a static IV to the new GCM encryption method.
|
// legacy CBC encryption with a static IV to the new GCM encryption method.
|
||||||
func migrateLegacyEncryptedUsersToGCM(ctx context.Context, crypt *FieldEncrypt, db *sql.DB) error {
|
func migrateLegacyEncryptedUsersToGCM(ctx context.Context, db *gorm.DB, crypt *FieldEncrypt) error {
|
||||||
log.WithContext(ctx).Debug("Migrating CBC encrypted deleted users to GCM")
|
model := &activity.DeletedUser{}
|
||||||
|
|
||||||
tx, err := db.Begin()
|
if !db.Migrator().HasTable(model) {
|
||||||
if err != nil {
|
log.WithContext(ctx).Debugf("Table for %T does not exist, no CBC to GCM migration needed", model)
|
||||||
return fmt.Errorf("failed to begin transaction: %v", err)
|
return nil
|
||||||
}
|
}
|
||||||
defer func() {
|
|
||||||
_ = tx.Rollback()
|
|
||||||
}()
|
|
||||||
|
|
||||||
rows, err := tx.Query(fmt.Sprintf(`SELECT id, email, name FROM deleted_users where enc_algo IS NULL OR enc_algo != '%s'`, gcmEncAlgo))
|
var deletedUsers []activity.DeletedUser
|
||||||
|
err := db.Model(model).Find(&deletedUsers, "enc_algo IS NULL OR enc_algo != ?", gcmEncAlgo).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to execute select query: %v", err)
|
return fmt.Errorf("failed to query deleted_users: %w", err)
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
updateStmt, err := tx.Prepare(`UPDATE deleted_users SET email = ?, name = ?, enc_algo = ? WHERE id = ?`)
|
if len(deletedUsers) == 0 {
|
||||||
if err != nil {
|
log.WithContext(ctx).Debug("No CBC encrypted deleted users to migrate")
|
||||||
return fmt.Errorf("failed to prepare update statement: %v", err)
|
return nil
|
||||||
}
|
}
|
||||||
defer updateStmt.Close()
|
|
||||||
|
|
||||||
if err = processUserRows(ctx, crypt, rows, updateStmt); err != nil {
|
if err = db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
for _, user := range deletedUsers {
|
||||||
|
if err = updateDeletedUserData(tx, user, crypt); err != nil {
|
||||||
|
return fmt.Errorf("failed to migrate deleted user %s: %w", user.ID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = tx.Commit(); err != nil {
|
|
||||||
return fmt.Errorf("failed to commit transaction: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.WithContext(ctx).Debug("Successfully migrated CBC encrypted deleted users to GCM")
|
log.WithContext(ctx).Debug("Successfully migrated CBC encrypted deleted users to GCM")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// processUserRows processes database rows of user data, decrypts legacy encryption fields, and re-encrypts them using GCM.
|
func updateDeletedUserData(transaction *gorm.DB, user activity.DeletedUser, crypt *FieldEncrypt) error {
|
||||||
func processUserRows(ctx context.Context, crypt *FieldEncrypt, rows *sql.Rows, updateStmt *sql.Stmt) error {
|
var err error
|
||||||
for rows.Next() {
|
var decryptedEmail, decryptedName string
|
||||||
var (
|
|
||||||
id, decryptedEmail, decryptedName string
|
|
||||||
email, name *string
|
|
||||||
)
|
|
||||||
|
|
||||||
err := rows.Scan(&id, &email, &name)
|
if user.Email != "" {
|
||||||
|
decryptedEmail, err = crypt.LegacyDecrypt(user.Email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("failed to decrypt email: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if email != nil {
|
if user.Name != "" {
|
||||||
decryptedEmail, err = crypt.LegacyDecrypt(*email)
|
decryptedName, err = crypt.LegacyDecrypt(user.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithContext(ctx).Warnf("skipping migrating deleted user %s: %v",
|
return fmt.Errorf("failed to decrypt name: %w", err)
|
||||||
id,
|
|
||||||
fmt.Errorf("failed to decrypt email: %w", err),
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if name != nil {
|
updatedUser := user
|
||||||
decryptedName, err = crypt.LegacyDecrypt(*name)
|
updatedUser.EncAlgo = gcmEncAlgo
|
||||||
if err != nil {
|
|
||||||
log.WithContext(ctx).Warnf("skipping migrating deleted user %s: %v",
|
|
||||||
id,
|
|
||||||
fmt.Errorf("failed to decrypt name: %w", err),
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
encryptedEmail, err := crypt.Encrypt(decryptedEmail)
|
updatedUser.Email, err = crypt.Encrypt(decryptedEmail)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to encrypt email: %w", err)
|
return fmt.Errorf("failed to encrypt email: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
encryptedName, err := crypt.Encrypt(decryptedName)
|
updatedUser.Name, err = crypt.Encrypt(decryptedName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to encrypt name: %w", err)
|
return fmt.Errorf("failed to encrypt name: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = updateStmt.Exec(encryptedEmail, encryptedName, gcmEncAlgo, id)
|
return transaction.Model(&updatedUser).Omit("id").Updates(updatedUser).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// MigrateDuplicateDeletedUsers removes duplicates and ensures the id column is marked as the primary key
|
||||||
|
func migrateDuplicateDeletedUsers(ctx context.Context, db *gorm.DB) error {
|
||||||
|
model := &activity.DeletedUser{}
|
||||||
|
if !db.Migrator().HasTable(model) {
|
||||||
|
log.WithContext(ctx).Debugf("Table for %T does not exist, no duplicate migration needed", model)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
isPrimaryKey, err := isColumnPrimaryKey[activity.DeletedUser](db, "id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isPrimaryKey {
|
||||||
|
log.WithContext(ctx).Debug("No duplicate deleted users to migrate")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := rows.Err(); err != nil {
|
if err = db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
groupById := tx.Model(model).Select("MAX(rowid)").Group("id")
|
||||||
|
if err = tx.Delete(model, "rowid NOT IN (?)", groupById).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = tx.Migrator().RenameTable("deleted_users", "deleted_users_old"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = tx.Migrator().CreateTable(model); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = tx.Exec(`
|
||||||
|
INSERT INTO deleted_users (id, email, name, enc_algo) SELECT id, email, name, enc_algo
|
||||||
|
FROM deleted_users_old;`).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Migrator().DropTable("deleted_users_old")
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WithContext(ctx).Debug("Successfully migrated duplicate deleted users")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isColumnPrimaryKey checks if a column is a primary key in the given model
|
||||||
|
func isColumnPrimaryKey[T any](db *gorm.DB, columnName string) (bool, error) {
|
||||||
|
var model T
|
||||||
|
|
||||||
|
cols, err := db.Migrator().ColumnTypes(&model)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, col := range cols {
|
||||||
|
if col.Name() == columnName {
|
||||||
|
isPrimaryKey, _ := col.PrimaryKey()
|
||||||
|
return isPrimaryKey, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
@ -2,38 +2,39 @@ package sqlite
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
|
||||||
"github.com/netbirdio/netbird/management/server/activity"
|
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/management/server/activity"
|
||||||
|
"github.com/netbirdio/netbird/management/server/migration"
|
||||||
)
|
)
|
||||||
|
|
||||||
func setupDatabase(t *testing.T) *sql.DB {
|
const (
|
||||||
|
insertDeletedUserQuery = `INSERT INTO deleted_users (id, email, name, enc_algo) VALUES (?, ?, ?, ?)`
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupDatabase(t *testing.T) *gorm.DB {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
dbFile := filepath.Join(t.TempDir(), eventSinkDB)
|
dbFile := filepath.Join(t.TempDir(), eventSinkDB)
|
||||||
db, err := sql.Open("sqlite3", dbFile)
|
db, err := gorm.Open(sqlite.Open(dbFile))
|
||||||
require.NoError(t, err, "Failed to open database")
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
sql, err := db.DB()
|
||||||
|
require.NoError(t, err)
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
_ = db.Close()
|
_ = sql.Close()
|
||||||
})
|
})
|
||||||
|
|
||||||
_, err = db.Exec(createTableQuery)
|
|
||||||
require.NoError(t, err, "Failed to create events table")
|
|
||||||
|
|
||||||
_, err = db.Exec(`CREATE TABLE deleted_users (id TEXT NOT NULL, email TEXT NOT NULL, name TEXT);`)
|
|
||||||
require.NoError(t, err, "Failed to create deleted_users table")
|
|
||||||
|
|
||||||
return db
|
return db
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMigrate(t *testing.T) {
|
func TestMigrateLegacyEncryptedUsersToGCM(t *testing.T) {
|
||||||
db := setupDatabase(t)
|
db := setupDatabase(t)
|
||||||
|
|
||||||
key, err := GenerateKey()
|
key, err := GenerateKey()
|
||||||
@ -42,43 +43,98 @@ func TestMigrate(t *testing.T) {
|
|||||||
crypt, err := NewFieldEncrypt(key)
|
crypt, err := NewFieldEncrypt(key)
|
||||||
require.NoError(t, err, "Failed to initialize FieldEncrypt")
|
require.NoError(t, err, "Failed to initialize FieldEncrypt")
|
||||||
|
|
||||||
legacyEmail := crypt.LegacyEncrypt("testaccount@test.com")
|
t.Run("empty table, no migration required", func(t *testing.T) {
|
||||||
legacyName := crypt.LegacyEncrypt("Test Account")
|
require.NoError(t, migrateLegacyEncryptedUsersToGCM(context.Background(), db, crypt))
|
||||||
|
assert.False(t, db.Migrator().HasTable("deleted_users"))
|
||||||
|
})
|
||||||
|
|
||||||
_, err = db.Exec(`INSERT INTO events(activity, timestamp, initiator_id, target_id, account_id, meta) VALUES(?, ?, ?, ?, ?, ?)`,
|
require.NoError(t, db.Exec(`CREATE TABLE deleted_users (id TEXT NOT NULL, email TEXT NOT NULL, name TEXT);`).Error)
|
||||||
activity.UserDeleted, time.Now(), "initiatorID", "targetID", "accountID", "")
|
assert.True(t, db.Migrator().HasTable("deleted_users"))
|
||||||
require.NoError(t, err, "Failed to insert event")
|
assert.False(t, db.Migrator().HasColumn("deleted_users", "enc_algo"))
|
||||||
|
|
||||||
_, err = db.Exec(`INSERT INTO deleted_users(id, email, name) VALUES(?, ?, ?)`, "targetID", legacyEmail, legacyName)
|
require.NoError(t, migration.MigrateNewField[activity.DeletedUser](context.Background(), db, "enc_algo", ""))
|
||||||
require.NoError(t, err, "Failed to insert legacy encrypted data")
|
assert.True(t, db.Migrator().HasColumn("deleted_users", "enc_algo"))
|
||||||
|
|
||||||
colExists, err := checkColumnExists(db, "deleted_users", "enc_algo")
|
t.Run("legacy users migration", func(t *testing.T) {
|
||||||
require.NoError(t, err, "Failed to check if enc_algo column exists")
|
legacyEmail := crypt.LegacyEncrypt("test.user@test.com")
|
||||||
require.False(t, colExists, "enc_algo column should not exist before migration")
|
legacyName := crypt.LegacyEncrypt("Test User")
|
||||||
|
|
||||||
err = migrate(context.Background(), crypt, db)
|
require.NoError(t, db.Exec(insertDeletedUserQuery, "user1", legacyEmail, legacyName, "").Error)
|
||||||
require.NoError(t, err, "Migration failed")
|
require.NoError(t, db.Exec(insertDeletedUserQuery, "user2", legacyEmail, legacyName, "legacy").Error)
|
||||||
|
|
||||||
colExists, err = checkColumnExists(db, "deleted_users", "enc_algo")
|
require.NoError(t, migrateLegacyEncryptedUsersToGCM(context.Background(), db, crypt))
|
||||||
require.NoError(t, err, "Failed to check if enc_algo column exists after migration")
|
|
||||||
require.True(t, colExists, "enc_algo column should exist after migration")
|
|
||||||
|
|
||||||
var encAlgo string
|
var users []activity.DeletedUser
|
||||||
err = db.QueryRow(`SELECT enc_algo FROM deleted_users LIMIT 1`, "").Scan(&encAlgo)
|
require.NoError(t, db.Find(&users).Error)
|
||||||
require.NoError(t, err, "Failed to select updated data")
|
assert.Len(t, users, 2)
|
||||||
require.Equal(t, gcmEncAlgo, encAlgo, "enc_algo should be set to 'GCM' after migration")
|
|
||||||
|
|
||||||
store, err := createStore(crypt, db)
|
for _, user := range users {
|
||||||
require.NoError(t, err, "Failed to create store")
|
assert.Equal(t, gcmEncAlgo, user.EncAlgo)
|
||||||
|
|
||||||
events, err := store.Get(context.Background(), "accountID", 0, 1, false)
|
decryptedEmail, err := crypt.Decrypt(user.Email)
|
||||||
require.NoError(t, err, "Failed to get events")
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "test.user@test.com", decryptedEmail)
|
||||||
|
|
||||||
require.Len(t, events, 1, "Should have one event")
|
decryptedName, err := crypt.Decrypt(user.Name)
|
||||||
require.Equal(t, activity.UserDeleted, events[0].Activity, "activity should match")
|
require.NoError(t, err)
|
||||||
require.Equal(t, "initiatorID", events[0].InitiatorID, "initiator id should match")
|
require.Equal(t, "Test User", decryptedName)
|
||||||
require.Equal(t, "targetID", events[0].TargetID, "target id should match")
|
}
|
||||||
require.Equal(t, "accountID", events[0].AccountID, "account id should match")
|
})
|
||||||
require.Equal(t, "testaccount@test.com", events[0].Meta["email"], "email should match")
|
|
||||||
require.Equal(t, "Test Account", events[0].Meta["username"], "username should match")
|
t.Run("users already migrated, no migration", func(t *testing.T) {
|
||||||
|
encryptedEmail, err := crypt.Encrypt("test.user@test.com")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
encryptedName, err := crypt.Encrypt("Test User")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NoError(t, db.Exec(insertDeletedUserQuery, "user3", encryptedEmail, encryptedName, gcmEncAlgo).Error)
|
||||||
|
require.NoError(t, migrateLegacyEncryptedUsersToGCM(context.Background(), db, crypt))
|
||||||
|
|
||||||
|
var users []activity.DeletedUser
|
||||||
|
require.NoError(t, db.Find(&users).Error)
|
||||||
|
assert.Len(t, users, 3)
|
||||||
|
|
||||||
|
for _, user := range users {
|
||||||
|
assert.Equal(t, gcmEncAlgo, user.EncAlgo)
|
||||||
|
|
||||||
|
decryptedEmail, err := crypt.Decrypt(user.Email)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "test.user@test.com", decryptedEmail)
|
||||||
|
|
||||||
|
decryptedName, err := crypt.Decrypt(user.Name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "Test User", decryptedName)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMigrateDuplicateDeletedUsers(t *testing.T) {
|
||||||
|
db := setupDatabase(t)
|
||||||
|
|
||||||
|
require.NoError(t, migrateDuplicateDeletedUsers(context.Background(), db))
|
||||||
|
assert.False(t, db.Migrator().HasTable("deleted_users"))
|
||||||
|
|
||||||
|
require.NoError(t, db.Exec(`CREATE TABLE deleted_users (id TEXT NOT NULL, email TEXT NOT NULL, name TEXT, enc_algo TEXT NOT NULL);`).Error)
|
||||||
|
assert.True(t, db.Migrator().HasTable("deleted_users"))
|
||||||
|
|
||||||
|
isPrimaryKey, err := isColumnPrimaryKey[activity.DeletedUser](db, "id")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, isPrimaryKey)
|
||||||
|
|
||||||
|
require.NoError(t, db.Exec(insertDeletedUserQuery, "user1", "email1", "name1", "GCM").Error)
|
||||||
|
require.NoError(t, db.Exec(insertDeletedUserQuery, "user1", "email2", "name2", "GCM").Error)
|
||||||
|
require.NoError(t, migrateDuplicateDeletedUsers(context.Background(), db))
|
||||||
|
|
||||||
|
isPrimaryKey, err = isColumnPrimaryKey[activity.DeletedUser](db, "id")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, isPrimaryKey)
|
||||||
|
|
||||||
|
var users []activity.DeletedUser
|
||||||
|
require.NoError(t, db.Find(&users).Error)
|
||||||
|
assert.Len(t, users, 1)
|
||||||
|
assert.Equal(t, "user1", users[0].ID)
|
||||||
|
assert.Equal(t, "email2", users[0].Email)
|
||||||
|
assert.Equal(t, "name2", users[0].Name)
|
||||||
|
assert.Equal(t, "GCM", users[0].EncAlgo)
|
||||||
}
|
}
|
||||||
|
@ -2,15 +2,14 @@ package sqlite
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/server/activity"
|
"github.com/netbirdio/netbird/management/server/activity"
|
||||||
)
|
)
|
||||||
@ -18,59 +17,6 @@ import (
|
|||||||
const (
|
const (
|
||||||
// eventSinkDB is the default name of the events database
|
// eventSinkDB is the default name of the events database
|
||||||
eventSinkDB = "events.db"
|
eventSinkDB = "events.db"
|
||||||
createTableQuery = "CREATE TABLE IF NOT EXISTS events " +
|
|
||||||
"(id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
|
||||||
"activity INTEGER, " +
|
|
||||||
"timestamp DATETIME, " +
|
|
||||||
"initiator_id TEXT," +
|
|
||||||
"account_id TEXT," +
|
|
||||||
"meta TEXT," +
|
|
||||||
" target_id TEXT);"
|
|
||||||
|
|
||||||
creatTableDeletedUsersQuery = `CREATE TABLE IF NOT EXISTS deleted_users (id TEXT NOT NULL, email TEXT NOT NULL, name TEXT, enc_algo TEXT NOT NULL);`
|
|
||||||
|
|
||||||
selectDescQuery = `SELECT events.id, activity, timestamp, initiator_id, i.name as "initiator_name", i.email as "initiator_email", target_id, t.name as "target_name", t.email as "target_email", account_id, meta
|
|
||||||
FROM events
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT id, MAX(name) as name, MAX(email) as email
|
|
||||||
FROM deleted_users
|
|
||||||
GROUP BY id
|
|
||||||
) i ON events.initiator_id = i.id
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT id, MAX(name) as name, MAX(email) as email
|
|
||||||
FROM deleted_users
|
|
||||||
GROUP BY id
|
|
||||||
) t ON events.target_id = t.id
|
|
||||||
WHERE account_id = ?
|
|
||||||
ORDER BY timestamp DESC LIMIT ? OFFSET ?;`
|
|
||||||
|
|
||||||
selectAscQuery = `SELECT events.id, activity, timestamp, initiator_id, i.name as "initiator_name", i.email as "initiator_email", target_id, t.name as "target_name", t.email as "target_email", account_id, meta
|
|
||||||
FROM events
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT id, MAX(name) as name, MAX(email) as email
|
|
||||||
FROM deleted_users
|
|
||||||
GROUP BY id
|
|
||||||
) i ON events.initiator_id = i.id
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT id, MAX(name) as name, MAX(email) as email
|
|
||||||
FROM deleted_users
|
|
||||||
GROUP BY id
|
|
||||||
) t ON events.target_id = t.id
|
|
||||||
WHERE account_id = ?
|
|
||||||
ORDER BY timestamp ASC LIMIT ? OFFSET ?;`
|
|
||||||
|
|
||||||
insertQuery = "INSERT INTO events(activity, timestamp, initiator_id, target_id, account_id, meta) " +
|
|
||||||
"VALUES(?, ?, ?, ?, ?, ?)"
|
|
||||||
|
|
||||||
/*
|
|
||||||
TODO:
|
|
||||||
The insert should avoid duplicated IDs in the table. So the query should be changes to something like:
|
|
||||||
`INSERT INTO deleted_users(id, email, name) VALUES(?, ?, ?) ON CONFLICT (id) DO UPDATE SET email = EXCLUDED.email, name = EXCLUDED.name;`
|
|
||||||
For this to work we have to set the id column as primary key. But this is not possible because the id column is not unique
|
|
||||||
and some selfhosted deployments might have duplicates already so we need to clean the table first.
|
|
||||||
*/
|
|
||||||
|
|
||||||
insertDeleteUserQuery = `INSERT INTO deleted_users(id, email, name, enc_algo) VALUES(?, ?, ?, ?)`
|
|
||||||
|
|
||||||
fallbackName = "unknown"
|
fallbackName = "unknown"
|
||||||
fallbackEmail = "unknown@unknown.com"
|
fallbackEmail = "unknown@unknown.com"
|
||||||
@ -78,172 +24,158 @@ const (
|
|||||||
gcmEncAlgo = "GCM"
|
gcmEncAlgo = "GCM"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type eventWithNames struct {
|
||||||
|
activity.Event
|
||||||
|
InitiatorName string
|
||||||
|
InitiatorEmail string
|
||||||
|
TargetName string
|
||||||
|
TargetEmail string
|
||||||
|
}
|
||||||
|
|
||||||
// Store is the implementation of the activity.Store interface backed by SQLite
|
// Store is the implementation of the activity.Store interface backed by SQLite
|
||||||
type Store struct {
|
type Store struct {
|
||||||
db *sql.DB
|
db *gorm.DB
|
||||||
fieldEncrypt *FieldEncrypt
|
fieldEncrypt *FieldEncrypt
|
||||||
|
|
||||||
insertStatement *sql.Stmt
|
|
||||||
selectAscStatement *sql.Stmt
|
|
||||||
selectDescStatement *sql.Stmt
|
|
||||||
deleteUserStmt *sql.Stmt
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSQLiteStore creates a new Store with an event table if not exists.
|
// NewSQLiteStore creates a new Store with an event table if not exists.
|
||||||
func NewSQLiteStore(ctx context.Context, dataDir string, encryptionKey string) (*Store, error) {
|
func NewSQLiteStore(ctx context.Context, dataDir string, encryptionKey string) (*Store, error) {
|
||||||
dbFile := filepath.Join(dataDir, eventSinkDB)
|
|
||||||
db, err := sql.Open("sqlite3", dbFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
db.SetMaxOpenConns(runtime.NumCPU())
|
|
||||||
|
|
||||||
crypt, err := NewFieldEncrypt(encryptionKey)
|
crypt, err := NewFieldEncrypt(encryptionKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = db.Close()
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dbFile := filepath.Join(dataDir, eventSinkDB)
|
||||||
|
db, err := gorm.Open(sqlite.Open(dbFile), &gorm.Config{
|
||||||
|
Logger: logger.Default.LogMode(logger.Silent),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sql, err := db.DB()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sql.SetMaxOpenConns(1)
|
||||||
|
|
||||||
if err = migrate(ctx, crypt, db); err != nil {
|
if err = migrate(ctx, crypt, db); err != nil {
|
||||||
_ = db.Close()
|
|
||||||
return nil, fmt.Errorf("events database migration: %w", err)
|
return nil, fmt.Errorf("events database migration: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return createStore(crypt, db)
|
err = db.AutoMigrate(&activity.Event{}, &activity.DeletedUser{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("events auto migrate: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *Store) processResult(ctx context.Context, result *sql.Rows) ([]*activity.Event, error) {
|
return &Store{
|
||||||
events := make([]*activity.Event, 0)
|
db: db,
|
||||||
|
fieldEncrypt: crypt,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *Store) processResult(ctx context.Context, events []*eventWithNames) ([]*activity.Event, error) {
|
||||||
|
activityEvents := make([]*activity.Event, 0)
|
||||||
var cryptErr error
|
var cryptErr error
|
||||||
for result.Next() {
|
|
||||||
var id int64
|
for _, event := range events {
|
||||||
var operation activity.Activity
|
e := event.Event
|
||||||
var timestamp time.Time
|
if e.Meta == nil {
|
||||||
var initiator string
|
e.Meta = make(map[string]any)
|
||||||
var initiatorName *string
|
|
||||||
var initiatorEmail *string
|
|
||||||
var target string
|
|
||||||
var targetUserName *string
|
|
||||||
var targetEmail *string
|
|
||||||
var account string
|
|
||||||
var jsonMeta string
|
|
||||||
err := result.Scan(&id, &operation, ×tamp, &initiator, &initiatorName, &initiatorEmail, &target, &targetUserName, &targetEmail, &account, &jsonMeta)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
meta := make(map[string]any)
|
if event.TargetName != "" {
|
||||||
if jsonMeta != "" {
|
name, err := store.fieldEncrypt.Decrypt(event.TargetName)
|
||||||
err = json.Unmarshal([]byte(jsonMeta), &meta)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
cryptErr = fmt.Errorf("failed to decrypt username for target id: %s", event.TargetName)
|
||||||
}
|
e.Meta["username"] = fallbackName
|
||||||
}
|
|
||||||
|
|
||||||
if targetUserName != nil {
|
|
||||||
name, err := store.fieldEncrypt.Decrypt(*targetUserName)
|
|
||||||
if err != nil {
|
|
||||||
cryptErr = fmt.Errorf("failed to decrypt username for target id: %s", target)
|
|
||||||
meta["username"] = fallbackName
|
|
||||||
} else {
|
} else {
|
||||||
meta["username"] = name
|
e.Meta["username"] = name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if targetEmail != nil {
|
if event.TargetEmail != "" {
|
||||||
email, err := store.fieldEncrypt.Decrypt(*targetEmail)
|
email, err := store.fieldEncrypt.Decrypt(event.TargetEmail)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cryptErr = fmt.Errorf("failed to decrypt email address for target id: %s", target)
|
cryptErr = fmt.Errorf("failed to decrypt email address for target id: %s", event.TargetEmail)
|
||||||
meta["email"] = fallbackEmail
|
e.Meta["email"] = fallbackEmail
|
||||||
} else {
|
} else {
|
||||||
meta["email"] = email
|
e.Meta["email"] = email
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
event := &activity.Event{
|
if event.InitiatorName != "" {
|
||||||
Timestamp: timestamp,
|
name, err := store.fieldEncrypt.Decrypt(event.InitiatorName)
|
||||||
Activity: operation,
|
|
||||||
ID: uint64(id),
|
|
||||||
InitiatorID: initiator,
|
|
||||||
TargetID: target,
|
|
||||||
AccountID: account,
|
|
||||||
Meta: meta,
|
|
||||||
}
|
|
||||||
|
|
||||||
if initiatorName != nil {
|
|
||||||
name, err := store.fieldEncrypt.Decrypt(*initiatorName)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cryptErr = fmt.Errorf("failed to decrypt username of initiator: %s", initiator)
|
cryptErr = fmt.Errorf("failed to decrypt username of initiator: %s", event.InitiatorName)
|
||||||
event.InitiatorName = fallbackName
|
e.InitiatorName = fallbackName
|
||||||
} else {
|
} else {
|
||||||
event.InitiatorName = name
|
e.InitiatorName = name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if initiatorEmail != nil {
|
if event.InitiatorEmail != "" {
|
||||||
email, err := store.fieldEncrypt.Decrypt(*initiatorEmail)
|
email, err := store.fieldEncrypt.Decrypt(event.InitiatorEmail)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cryptErr = fmt.Errorf("failed to decrypt email address of initiator: %s", initiator)
|
cryptErr = fmt.Errorf("failed to decrypt email address of initiator: %s", event.InitiatorEmail)
|
||||||
event.InitiatorEmail = fallbackEmail
|
e.InitiatorEmail = fallbackEmail
|
||||||
} else {
|
} else {
|
||||||
event.InitiatorEmail = email
|
e.InitiatorEmail = email
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
events = append(events, event)
|
activityEvents = append(activityEvents, &e)
|
||||||
}
|
}
|
||||||
|
|
||||||
if cryptErr != nil {
|
if cryptErr != nil {
|
||||||
log.WithContext(ctx).Warnf("%s", cryptErr)
|
log.WithContext(ctx).Warnf("%s", cryptErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
return events, nil
|
return activityEvents, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns "limit" number of events from index ordered descending or ascending by a timestamp
|
// Get returns "limit" number of events from index ordered descending or ascending by a timestamp
|
||||||
func (store *Store) Get(ctx context.Context, accountID string, offset, limit int, descending bool) ([]*activity.Event, error) {
|
func (store *Store) Get(ctx context.Context, accountID string, offset, limit int, descending bool) ([]*activity.Event, error) {
|
||||||
stmt := store.selectDescStatement
|
baseQuery := store.db.Model(&activity.Event{}).
|
||||||
|
Select(`
|
||||||
|
events.*,
|
||||||
|
u.name AS initiator_name,
|
||||||
|
u.email AS initiator_email,
|
||||||
|
t.name AS target_name,
|
||||||
|
t.email AS target_email
|
||||||
|
`).
|
||||||
|
Joins(`LEFT JOIN deleted_users u ON u.id = events.initiator_id`).
|
||||||
|
Joins(`LEFT JOIN deleted_users t ON t.id = events.target_id`)
|
||||||
|
|
||||||
|
orderDir := "DESC"
|
||||||
if !descending {
|
if !descending {
|
||||||
stmt = store.selectAscStatement
|
orderDir = "ASC"
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := stmt.Query(accountID, limit, offset)
|
var events []*eventWithNames
|
||||||
|
err := baseQuery.Order("events.timestamp "+orderDir).Offset(offset).Limit(limit).
|
||||||
|
Find(&events, "account_id = ?", accountID).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer result.Close() //nolint
|
return store.processResult(ctx, events)
|
||||||
return store.processResult(ctx, result)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save an event in the SQLite events table end encrypt the "email" element in meta map
|
// Save an event in the SQLite events table end encrypt the "email" element in meta map
|
||||||
func (store *Store) Save(_ context.Context, event *activity.Event) (*activity.Event, error) {
|
func (store *Store) Save(_ context.Context, event *activity.Event) (*activity.Event, error) {
|
||||||
var jsonMeta string
|
|
||||||
meta, err := store.saveDeletedUserEmailAndNameInEncrypted(event)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if meta != nil {
|
|
||||||
metaBytes, err := json.Marshal(event.Meta)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
jsonMeta = string(metaBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := store.insertStatement.Exec(event.Activity, event.Timestamp, event.InitiatorID, event.TargetID, event.AccountID, jsonMeta)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := result.LastInsertId()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
eventCopy := event.Copy()
|
eventCopy := event.Copy()
|
||||||
eventCopy.ID = uint64(id)
|
meta, err := store.saveDeletedUserEmailAndNameInEncrypted(eventCopy)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
eventCopy.Meta = meta
|
||||||
|
|
||||||
|
if err = store.db.Create(eventCopy).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return eventCopy, nil
|
return eventCopy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -260,16 +192,27 @@ func (store *Store) saveDeletedUserEmailAndNameInEncrypted(event *activity.Event
|
|||||||
return event.Meta, nil
|
return event.Meta, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deletedUser := activity.DeletedUser{
|
||||||
|
ID: event.TargetID,
|
||||||
|
EncAlgo: gcmEncAlgo,
|
||||||
|
}
|
||||||
|
|
||||||
encryptedEmail, err := store.fieldEncrypt.Encrypt(fmt.Sprintf("%s", email))
|
encryptedEmail, err := store.fieldEncrypt.Encrypt(fmt.Sprintf("%s", email))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
deletedUser.Email = encryptedEmail
|
||||||
|
|
||||||
encryptedName, err := store.fieldEncrypt.Encrypt(fmt.Sprintf("%s", name))
|
encryptedName, err := store.fieldEncrypt.Encrypt(fmt.Sprintf("%s", name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
deletedUser.Name = encryptedName
|
||||||
|
|
||||||
_, err = store.deleteUserStmt.Exec(event.TargetID, encryptedEmail, encryptedName, gcmEncAlgo)
|
err = store.db.Clauses(clause.OnConflict{
|
||||||
|
Columns: []clause.Column{{Name: "id"}},
|
||||||
|
DoUpdates: clause.AssignmentColumns([]string{"email", "name"}),
|
||||||
|
}).Create(deletedUser).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -285,75 +228,11 @@ func (store *Store) saveDeletedUserEmailAndNameInEncrypted(event *activity.Event
|
|||||||
// Close the Store
|
// Close the Store
|
||||||
func (store *Store) Close(_ context.Context) error {
|
func (store *Store) Close(_ context.Context) error {
|
||||||
if store.db != nil {
|
if store.db != nil {
|
||||||
return store.db.Close()
|
sql, err := store.db.DB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return sql.Close()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// createStore initializes and returns a new Store instance with prepared SQL statements.
|
|
||||||
func createStore(crypt *FieldEncrypt, db *sql.DB) (*Store, error) {
|
|
||||||
insertStmt, err := db.Prepare(insertQuery)
|
|
||||||
if err != nil {
|
|
||||||
_ = db.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
selectDescStmt, err := db.Prepare(selectDescQuery)
|
|
||||||
if err != nil {
|
|
||||||
_ = db.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
selectAscStmt, err := db.Prepare(selectAscQuery)
|
|
||||||
if err != nil {
|
|
||||||
_ = db.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteUserStmt, err := db.Prepare(insertDeleteUserQuery)
|
|
||||||
if err != nil {
|
|
||||||
_ = db.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Store{
|
|
||||||
db: db,
|
|
||||||
fieldEncrypt: crypt,
|
|
||||||
insertStatement: insertStmt,
|
|
||||||
selectDescStatement: selectDescStmt,
|
|
||||||
selectAscStatement: selectAscStmt,
|
|
||||||
deleteUserStmt: deleteUserStmt,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkColumnExists checks if a column exists in a specified table
|
|
||||||
func checkColumnExists(db *sql.DB, tableName, columnName string) (bool, error) {
|
|
||||||
query := fmt.Sprintf("PRAGMA table_info(%s);", tableName)
|
|
||||||
rows, err := db.Query(query)
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("failed to query table info: %w", err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
for rows.Next() {
|
|
||||||
var cid int
|
|
||||||
var name, ctype string
|
|
||||||
var notnull, pk int
|
|
||||||
var dfltValue sql.NullString
|
|
||||||
|
|
||||||
err = rows.Scan(&cid, &name, &ctype, ¬null, &dfltValue, &pk)
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("failed to scan row: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if name == columnName {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = rows.Err(); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
@ -66,7 +66,7 @@ func (am *DefaultAccountManager) StoreEvent(ctx context.Context, initiatorID, ta
|
|||||||
go func() {
|
go func() {
|
||||||
_, err := am.eventStore.Save(ctx, &activity.Event{
|
_, err := am.eventStore.Save(ctx, &activity.Event{
|
||||||
Timestamp: time.Now().UTC(),
|
Timestamp: time.Now().UTC(),
|
||||||
Activity: activityID,
|
Activity: activityID.(activity.Activity),
|
||||||
InitiatorID: initiatorID,
|
InitiatorID: initiatorID,
|
||||||
TargetID: targetID,
|
TargetID: targetID,
|
||||||
AccountID: accountID,
|
AccountID: accountID,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user