mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-10-22 22:10:51 +02:00
Add gomftctl CLI tool and encryption key rotation utilities
This commit is contained in:
1 parent
495b844c00
commit
b9acf47530
17 files changed
+1754
-125
No files matched your search
@@ -0,0 +1,772 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/starfleetcptn/gomft/internal/config"
|
||||
"github.com/starfleetcptn/gomft/internal/db"
|
||||
"github.com/starfleetcptn/gomft/internal/encryption"
|
||||
"github.com/starfleetcptn/gomft/internal/encryption/keyrotation"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create root command
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "gomftctl",
|
||||
Short: "GoMFT Control Tool - Command line utilities for GoMFT",
|
||||
Long: `GoMFT Control Tool (gomftctl) provides command line utilities for managing
|
||||
your GoMFT installation, including database migrations, security key rotation,
|
||||
and other administrative functions.`,
|
||||
}
|
||||
|
||||
// Add commands
|
||||
rootCmd.AddCommand(createMigrateCmd())
|
||||
rootCmd.AddCommand(createKeyRotationCmd())
|
||||
rootCmd.AddCommand(createVersionCmd())
|
||||
rootCmd.AddCommand(createBackupCmd())
|
||||
rootCmd.AddCommand(createUserCmd())
|
||||
rootCmd.AddCommand(createEncryptionKeyRotationCmd())
|
||||
|
||||
// Execute the root command
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// createMigrateCmd creates the migrate command for provider data migration
|
||||
func createMigrateCmd() *cobra.Command {
|
||||
var dryRun, validationOnly, force, debugMode, autoFill bool
|
||||
var backupDir string
|
||||
|
||||
migrateCmd := &cobra.Command{
|
||||
Use: "migrate-providers",
|
||||
Short: "Migrate provider data to the new storage provider model",
|
||||
Long: `Migrate provider data extracts unique provider configurations from existing
|
||||
transfer configs and creates dedicated storage provider records.
|
||||
|
||||
This command should be run when upgrading from older versions of GoMFT that
|
||||
stored provider configuration directly in transfer configs.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Load configuration
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load configuration: %v", err)
|
||||
}
|
||||
|
||||
// Set backup directory if not provided
|
||||
if backupDir == "" {
|
||||
backupDir = cfg.BackupDir
|
||||
}
|
||||
|
||||
// Initialize database
|
||||
dbPath := filepath.Join(cfg.DataDir, "gomft.db")
|
||||
database, err := db.Initialize(dbPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize database: %v", err)
|
||||
}
|
||||
defer database.Close()
|
||||
|
||||
// Create migration options
|
||||
options := db.MigrateProviderDataOptions{
|
||||
DryRun: dryRun,
|
||||
ValidationOnly: validationOnly,
|
||||
Force: force,
|
||||
BackupDir: backupDir,
|
||||
DebugMode: debugMode,
|
||||
AutoFill: autoFill,
|
||||
}
|
||||
|
||||
// Run migration
|
||||
fmt.Println("Starting provider data migration...")
|
||||
stats, err := database.MigrateProviderData(options)
|
||||
if err != nil {
|
||||
fmt.Println("\nMigration failed with error:")
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
|
||||
// Add more detailed error information
|
||||
fmt.Println("\nDetailed error information:")
|
||||
fmt.Println("===========================")
|
||||
|
||||
// Unwrap nested errors if possible
|
||||
var currentErr error = err
|
||||
depth := 1
|
||||
for currentErr != nil {
|
||||
fmt.Printf("%d. %v\n", depth, currentErr)
|
||||
if unwrapped, ok := currentErr.(interface{ Unwrap() error }); ok {
|
||||
currentErr = unwrapped.Unwrap()
|
||||
depth++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Print database connection information (without sensitive details)
|
||||
fmt.Println("\nDatabase information:")
|
||||
fmt.Printf("- Database path: %s\n", dbPath)
|
||||
fmt.Printf("- Migration options: dryRun=%v, validationOnly=%v, force=%v\n",
|
||||
options.DryRun, options.ValidationOnly, options.Force)
|
||||
|
||||
log.Fatalf("Migration failed. See details above.")
|
||||
}
|
||||
|
||||
// Print report
|
||||
fmt.Println(db.FormatMigrationReport(stats))
|
||||
},
|
||||
}
|
||||
|
||||
// Add flags
|
||||
migrateCmd.Flags().BoolVar(&dryRun, "dry-run", false, "Simulate migration without making changes")
|
||||
migrateCmd.Flags().BoolVar(&validationOnly, "validate-only", false, "Only validate if migration is possible without making changes")
|
||||
migrateCmd.Flags().BoolVar(&force, "force", false, "Force migration even if validation fails")
|
||||
migrateCmd.Flags().StringVar(&backupDir, "backup-dir", "", "Directory to store backup data (defaults to config backup_dir)")
|
||||
migrateCmd.Flags().BoolVar(&debugMode, "debug", false, "Enable debug mode with more detailed error messages")
|
||||
migrateCmd.Flags().BoolVar(&autoFill, "auto-fill", false, "Automatically fill missing required fields with placeholder values")
|
||||
|
||||
return migrateCmd
|
||||
}
|
||||
|
||||
// createKeyRotationCmd creates the key rotation command
|
||||
func createKeyRotationCmd() *cobra.Command {
|
||||
var keyType string
|
||||
var writeToEnv bool
|
||||
|
||||
keyRotationCmd := &cobra.Command{
|
||||
Use: "rotate-key",
|
||||
Short: "Rotate security keys used by GoMFT",
|
||||
Long: `Rotate security keys generates new cryptographic keys for GoMFT.
|
||||
|
||||
Available key types:
|
||||
- jwt: JSON Web Token signing key
|
||||
- totp: TOTP encryption key
|
||||
- encryption: General encryption key used for sensitive data
|
||||
|
||||
This command will generate a new key and provide instructions for updating
|
||||
your configuration. The application must be restarted for changes to take effect.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Validate key type
|
||||
validTypes := map[string]string{
|
||||
"jwt": "JWT_SECRET",
|
||||
"totp": "TOTP_ENCRYPTION_KEY",
|
||||
"encryption": "GOMFT_ENCRYPTION_KEY",
|
||||
}
|
||||
|
||||
envVar, valid := validTypes[keyType]
|
||||
if !valid {
|
||||
log.Fatalf("Invalid key type: %s. Valid types are: jwt, totp, encryption", keyType)
|
||||
}
|
||||
|
||||
// Load configuration
|
||||
_, err := config.Load()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load configuration: %v", err)
|
||||
}
|
||||
|
||||
// Generate a new key
|
||||
newKey, err := generateSecureKey()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to generate secure key: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Generated new %s key: %s\n\n", keyType, newKey)
|
||||
|
||||
if writeToEnv {
|
||||
// Read current .env file
|
||||
envPath := ".env"
|
||||
envContent, err := os.ReadFile(envPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read .env file: %v", err)
|
||||
}
|
||||
|
||||
// Update .env file with new key
|
||||
updatedEnv, updated := updateEnvVar(string(envContent), envVar, newKey)
|
||||
if !updated {
|
||||
// If the variable wasn't found, append it
|
||||
updatedEnv = updatedEnv + fmt.Sprintf("\n%s=%s\n", envVar, newKey)
|
||||
}
|
||||
|
||||
// Write updated content back to .env file
|
||||
if err := os.WriteFile(envPath, []byte(updatedEnv), 0644); err != nil {
|
||||
log.Fatalf("Failed to write updated .env file: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Updated %s in .env file\n", envVar)
|
||||
fmt.Println("Please restart the GoMFT application for changes to take effect.")
|
||||
} else {
|
||||
// Print instructions for manual update
|
||||
fmt.Println("To use this key, update your .env file with:")
|
||||
fmt.Printf("%s=%s\n\n", envVar, newKey)
|
||||
fmt.Println("Then restart the GoMFT application for changes to take effect.")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Add flags
|
||||
keyRotationCmd.Flags().StringVar(&keyType, "type", "", "Type of key to rotate (jwt, totp, encryption)")
|
||||
keyRotationCmd.Flags().BoolVar(&writeToEnv, "write", false, "Write the new key directly to .env file")
|
||||
keyRotationCmd.MarkFlagRequired("type")
|
||||
|
||||
return keyRotationCmd
|
||||
}
|
||||
|
||||
// createVersionCmd creates the version command
|
||||
func createVersionCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Display version information",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Import the version from the components package
|
||||
fmt.Println("GoMFT Control Tool")
|
||||
fmt.Println("Version: Same as GoMFT application")
|
||||
fmt.Println("Visit https://github.com/starfleetcptn/gomft for more information")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// createBackupCmd creates the backup command
|
||||
func createBackupCmd() *cobra.Command {
|
||||
var outputDir string
|
||||
|
||||
backupCmd := &cobra.Command{
|
||||
Use: "backup",
|
||||
Short: "Create a backup of the GoMFT database",
|
||||
Long: `Create a backup of the GoMFT database and configuration.
|
||||
The backup includes the SQLite database file and the .env configuration file.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Load configuration
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load configuration: %v", err)
|
||||
}
|
||||
|
||||
// Set output directory if not provided
|
||||
if outputDir == "" {
|
||||
outputDir = cfg.BackupDir
|
||||
}
|
||||
|
||||
// Ensure output directory exists
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
log.Fatalf("Failed to create backup directory: %v", err)
|
||||
}
|
||||
|
||||
// Create timestamp for backup filename
|
||||
timestamp := fmt.Sprintf("%s", filepath.Base(os.Args[0]))
|
||||
|
||||
// Create backup
|
||||
dbPath := filepath.Join(cfg.DataDir, "gomft.db")
|
||||
backupPath := filepath.Join(outputDir, fmt.Sprintf("gomft-backup-%s.db", timestamp))
|
||||
|
||||
// Copy database file
|
||||
if err := copyFile(dbPath, backupPath); err != nil {
|
||||
log.Fatalf("Failed to create database backup: %v", err)
|
||||
}
|
||||
|
||||
// Copy .env file if it exists
|
||||
envPath := ".env"
|
||||
backupEnvPath := filepath.Join(outputDir, fmt.Sprintf("gomft-env-backup-%s.env", timestamp))
|
||||
if _, err := os.Stat(envPath); err == nil {
|
||||
if err := copyFile(envPath, backupEnvPath); err != nil {
|
||||
log.Fatalf("Failed to backup .env file: %v", err)
|
||||
}
|
||||
fmt.Printf("Configuration backed up to: %s\n", backupEnvPath)
|
||||
}
|
||||
|
||||
fmt.Printf("Database backed up to: %s\n", backupPath)
|
||||
},
|
||||
}
|
||||
|
||||
// Add flags
|
||||
backupCmd.Flags().StringVar(&outputDir, "output-dir", "", "Directory to store backup files (defaults to config backup_dir)")
|
||||
|
||||
return backupCmd
|
||||
}
|
||||
|
||||
// createUserCmd creates the user management command
|
||||
func createUserCmd() *cobra.Command {
|
||||
userCmd := &cobra.Command{
|
||||
Use: "user",
|
||||
Short: "User management commands",
|
||||
Long: `Commands for managing GoMFT users, including creating, updating, and listing users.`,
|
||||
}
|
||||
|
||||
// Add subcommands
|
||||
userCmd.AddCommand(createUserCreateCmd())
|
||||
userCmd.AddCommand(createUserResetPasswordCmd())
|
||||
userCmd.AddCommand(createUserListCmd())
|
||||
|
||||
return userCmd
|
||||
}
|
||||
|
||||
// createUserCreateCmd creates the user create command
|
||||
func createUserCreateCmd() *cobra.Command {
|
||||
var email, password string
|
||||
var isAdmin bool
|
||||
|
||||
createCmd := &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create a new user",
|
||||
Long: `Create a new user with the specified email and password.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Load configuration
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load configuration: %v", err)
|
||||
}
|
||||
|
||||
// Initialize database
|
||||
dbPath := filepath.Join(cfg.DataDir, "gomft.db")
|
||||
database, err := db.Initialize(dbPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize database: %v", err)
|
||||
}
|
||||
defer database.Close()
|
||||
|
||||
// Create user by first generating password hash
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to hash password: %v", err)
|
||||
}
|
||||
|
||||
// Create user object
|
||||
user := &db.User{
|
||||
Email: email,
|
||||
PasswordHash: string(hashedPassword),
|
||||
LastPasswordChange: time.Now(),
|
||||
}
|
||||
|
||||
// Set admin status if requested
|
||||
if isAdmin {
|
||||
user.SetIsAdmin(true)
|
||||
}
|
||||
|
||||
// Save user to database
|
||||
if err := database.CreateUser(user); err != nil {
|
||||
log.Fatalf("Failed to create user: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("User created successfully:\n")
|
||||
fmt.Printf(" ID: %d\n", user.ID)
|
||||
fmt.Printf(" Email: %s\n", user.Email)
|
||||
fmt.Printf(" Admin: %t\n", user.GetIsAdmin())
|
||||
},
|
||||
}
|
||||
|
||||
// Add flags
|
||||
createCmd.Flags().StringVar(&email, "email", "", "User email address")
|
||||
createCmd.Flags().StringVar(&password, "password", "", "User password")
|
||||
createCmd.Flags().BoolVar(&isAdmin, "admin", false, "Grant admin privileges to the user")
|
||||
createCmd.MarkFlagRequired("email")
|
||||
createCmd.MarkFlagRequired("password")
|
||||
|
||||
return createCmd
|
||||
}
|
||||
|
||||
// createUserResetPasswordCmd creates the user reset-password command
|
||||
func createUserResetPasswordCmd() *cobra.Command {
|
||||
var email, newPassword string
|
||||
|
||||
resetCmd := &cobra.Command{
|
||||
Use: "reset-password",
|
||||
Short: "Reset a user's password",
|
||||
Long: `Reset the password for a user with the specified email address.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Load configuration
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load configuration: %v", err)
|
||||
}
|
||||
|
||||
// Initialize database
|
||||
dbPath := filepath.Join(cfg.DataDir, "gomft.db")
|
||||
database, err := db.Initialize(dbPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize database: %v", err)
|
||||
}
|
||||
defer database.Close()
|
||||
|
||||
// Find user by email
|
||||
var user db.User
|
||||
if err := database.Where("email = ?", email).First(&user).Error; err != nil {
|
||||
log.Fatalf("Failed to find user with email %s: %v", email, err)
|
||||
}
|
||||
|
||||
// Generate new password hash
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to hash password: %v", err)
|
||||
}
|
||||
|
||||
// Update user password
|
||||
user.PasswordHash = string(hashedPassword)
|
||||
user.LastPasswordChange = time.Now()
|
||||
|
||||
// Save user to database
|
||||
if err := database.Save(&user).Error; err != nil {
|
||||
log.Fatalf("Failed to update user: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Password reset successfully for user: %s\n", email)
|
||||
},
|
||||
}
|
||||
|
||||
// Add flags
|
||||
resetCmd.Flags().StringVar(&email, "email", "", "User email address")
|
||||
resetCmd.Flags().StringVar(&newPassword, "password", "", "New password")
|
||||
resetCmd.MarkFlagRequired("email")
|
||||
resetCmd.MarkFlagRequired("password")
|
||||
|
||||
return resetCmd
|
||||
}
|
||||
|
||||
// createUserListCmd creates the user list command
|
||||
func createUserListCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all users",
|
||||
Long: `List all users in the GoMFT system.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Load configuration
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load configuration: %v", err)
|
||||
}
|
||||
|
||||
// Initialize database
|
||||
dbPath := filepath.Join(cfg.DataDir, "gomft.db")
|
||||
database, err := db.Initialize(dbPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize database: %v", err)
|
||||
}
|
||||
defer database.Close()
|
||||
|
||||
// Get all users
|
||||
var users []db.User
|
||||
if err := database.Find(&users).Error; err != nil {
|
||||
log.Fatalf("Failed to get users: %v", err)
|
||||
}
|
||||
|
||||
// Print users
|
||||
fmt.Println("GoMFT Users:")
|
||||
fmt.Println("ID\tEmail\tAdmin\tLast Updated")
|
||||
fmt.Println("--------------------------------------------------")
|
||||
for _, user := range users {
|
||||
lastUpdated := "Never"
|
||||
if !user.UpdatedAt.IsZero() {
|
||||
lastUpdated = user.UpdatedAt.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
fmt.Printf("%d\t%s\t%t\t%s\n", user.ID, user.Email, user.GetIsAdmin(), lastUpdated)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// createEncryptionKeyRotationCmd creates the encryption key rotation command
|
||||
func createEncryptionKeyRotationCmd() *cobra.Command {
|
||||
var dryRun bool
|
||||
var batchSize, maxErrors int
|
||||
var backupDir string
|
||||
var skipBackup bool
|
||||
var oldKeyEnvVar string
|
||||
var modelsFlag string
|
||||
|
||||
rotateCmd := &cobra.Command{
|
||||
Use: "rotate-encryption-key",
|
||||
Short: "Rotate encryption keys for sensitive data",
|
||||
Long: `Rotate encryption keys for sensitive data stored in the database.
|
||||
|
||||
This command will:
|
||||
1. Create a backup of your database (unless --skip-backup is specified)
|
||||
2. Re-encrypt all sensitive data with a new encryption key
|
||||
3. Provide instructions for updating your configuration
|
||||
|
||||
The application must be stopped before running this command to prevent data corruption.
|
||||
`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Load configuration
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load configuration: %v", err)
|
||||
}
|
||||
|
||||
// Set backup directory if not provided
|
||||
if backupDir == "" {
|
||||
backupDir = cfg.BackupDir
|
||||
}
|
||||
|
||||
// Create backup if needed
|
||||
if !skipBackup {
|
||||
dbPath := filepath.Join(cfg.DataDir, "gomft.db")
|
||||
backupPath := filepath.Join(backupDir, fmt.Sprintf("gomft_backup_before_key_rotation_%s.db",
|
||||
time.Now().Format("20060102_150405")))
|
||||
|
||||
fmt.Printf("Creating database backup at %s...\n", backupPath)
|
||||
if err := copyFile(dbPath, backupPath); err != nil {
|
||||
log.Fatalf("Failed to create backup: %v", err)
|
||||
}
|
||||
fmt.Println("Backup created successfully.")
|
||||
} else {
|
||||
fmt.Println("Skipping database backup as requested.")
|
||||
}
|
||||
|
||||
// Initialize database
|
||||
dbPath := filepath.Join(cfg.DataDir, "gomft.db")
|
||||
database, err := db.Initialize(dbPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize database: %v", err)
|
||||
}
|
||||
defer database.Close()
|
||||
|
||||
// Setup old encryption service
|
||||
if oldKeyEnvVar == "" {
|
||||
oldKeyEnvVar = encryption.DefaultKeyEnvVar
|
||||
}
|
||||
|
||||
// Get the current encryption service
|
||||
oldService, err := encryption.GetGlobalEncryptionService()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get current encryption service: %v", err)
|
||||
}
|
||||
|
||||
// Generate new key
|
||||
newKey, err := encryption.GenerateKey(encryption.AES256KeySize)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to generate new encryption key: %v", err)
|
||||
}
|
||||
|
||||
// Create new key manager for the new key
|
||||
newKeyManager := &keyManager{key: newKey}
|
||||
|
||||
// Setup new encryption service with the new key
|
||||
newService, err := encryption.NewEncryptionService(newKeyManager)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create new encryption service: %v", err)
|
||||
}
|
||||
|
||||
// Create rotation options
|
||||
options := keyrotation.RotationOptions{
|
||||
DryRun: dryRun,
|
||||
BatchSize: batchSize,
|
||||
MaxErrors: maxErrors,
|
||||
Timeout: 24 * time.Hour,
|
||||
ProgressCallback: func(modelName string, processed, total int) {
|
||||
fmt.Printf("\rProcessing %s: %d/%d records (%.1f%%)",
|
||||
modelName, processed, total, float64(processed)/float64(total)*100)
|
||||
},
|
||||
}
|
||||
|
||||
// Create rotation utility
|
||||
rotationUtil, err := keyrotation.NewRotationUtility(
|
||||
database.DB, // Use the underlying gorm.DB
|
||||
oldService,
|
||||
newService,
|
||||
nil, // No auditor needed, keyrotation will use the global one
|
||||
options,
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create rotation utility: %v", err)
|
||||
}
|
||||
|
||||
// Find models with encrypted fields
|
||||
var models []interface{}
|
||||
if modelsFlag == "auto" {
|
||||
fmt.Println("Automatically detecting models with encrypted fields...")
|
||||
models, err = rotationUtil.FindModelsWithEncryptedFields()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to find models with encrypted fields: %v", err)
|
||||
}
|
||||
if len(models) == 0 {
|
||||
log.Fatalf("No models with encrypted fields found")
|
||||
}
|
||||
} else if modelsFlag != "" {
|
||||
// TODO: Support manual model specification
|
||||
log.Fatalf("Manual model specification not yet implemented, use --models=auto")
|
||||
} else {
|
||||
log.Fatalf("No models specified, use --models=auto to automatically detect models")
|
||||
}
|
||||
|
||||
// Create migration plan
|
||||
fmt.Println("Creating encryption migration plan...")
|
||||
plan, err := rotationUtil.CreateEncryptionMigrationPlan(models)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create migration plan: %v", err)
|
||||
}
|
||||
|
||||
// Print plan
|
||||
fmt.Println("\nEncryption Migration Plan:")
|
||||
fmt.Printf("Total models: %d\n", len(plan.ModelPlans))
|
||||
fmt.Printf("Total records: %d\n", plan.EstimatedRecords)
|
||||
fmt.Printf("Estimated duration: %s\n", plan.EstimatedDuration.Round(time.Second))
|
||||
fmt.Println("\nModels to process:")
|
||||
for name, modelPlan := range plan.ModelPlans {
|
||||
fmt.Printf("- %s: %d records, %d encrypted fields\n",
|
||||
name, modelPlan.RecordCount, len(modelPlan.EncryptedFields))
|
||||
}
|
||||
|
||||
// Confirm if not in dry run mode
|
||||
if !dryRun {
|
||||
fmt.Println("\nWARNING: This operation will re-encrypt all sensitive data with a new key.")
|
||||
fmt.Println("Make sure the application is stopped before proceeding.")
|
||||
fmt.Print("\nDo you want to continue? [y/N]: ")
|
||||
var response string
|
||||
fmt.Scanln(&response)
|
||||
if strings.ToLower(response) != "y" {
|
||||
fmt.Println("Operation cancelled.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Perform key rotation
|
||||
fmt.Println("\nStarting key rotation...")
|
||||
startTime := time.Now()
|
||||
stats, err := rotationUtil.RotateKeysForModels(context.Background(), models)
|
||||
if err != nil {
|
||||
fmt.Println("\nKey rotation failed with error:")
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
|
||||
// Add more detailed error information
|
||||
fmt.Println("\nDetailed error information:")
|
||||
fmt.Println("===========================")
|
||||
|
||||
// Unwrap nested errors if possible
|
||||
var currentErr error = err
|
||||
depth := 1
|
||||
for currentErr != nil {
|
||||
fmt.Printf("%d. %v\n", depth, currentErr)
|
||||
if unwrapped, ok := currentErr.(interface{ Unwrap() error }); ok {
|
||||
currentErr = unwrapped.Unwrap()
|
||||
depth++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Print rotation configuration details
|
||||
fmt.Println("\nRotation configuration:")
|
||||
fmt.Printf("- Dry run: %v\n", dryRun)
|
||||
fmt.Printf("- Batch size: %d\n", batchSize)
|
||||
fmt.Printf("- Max errors: %d\n", maxErrors)
|
||||
fmt.Printf("- Models: %s\n", modelsFlag)
|
||||
fmt.Printf("- Old key env var: %s\n", oldKeyEnvVar)
|
||||
|
||||
log.Fatalf("Key rotation failed. See details above.")
|
||||
}
|
||||
duration := time.Since(startTime).Round(time.Second)
|
||||
|
||||
// Print results
|
||||
fmt.Println("\nKey rotation completed successfully!")
|
||||
fmt.Printf("Total records processed: %d/%d\n", stats.ProcessedRecords, stats.TotalRecords)
|
||||
fmt.Printf("Failed records: %d\n", stats.FailedRecords)
|
||||
fmt.Printf("Duration: %s\n", duration)
|
||||
|
||||
if len(stats.Errors) > 0 {
|
||||
fmt.Printf("\nErrors (%d):\n", len(stats.Errors))
|
||||
for i, err := range stats.Errors {
|
||||
if i >= 10 {
|
||||
fmt.Printf("... and %d more errors\n", len(stats.Errors)-10)
|
||||
break
|
||||
}
|
||||
fmt.Printf("- %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Print next steps
|
||||
if !dryRun {
|
||||
fmt.Println("\nNext steps:")
|
||||
fmt.Println("1. Update your environment variable or .env file with the new encryption key:")
|
||||
fmt.Printf(" %s=%s\n", oldKeyEnvVar, base64.StdEncoding.EncodeToString(newKey))
|
||||
fmt.Println("2. Restart your GoMFT application")
|
||||
fmt.Println("\nIMPORTANT: Keep a backup of both the old and new keys until you verify everything works correctly.")
|
||||
} else {
|
||||
fmt.Println("\nDry run completed. No changes were made to the database.")
|
||||
fmt.Println("Run without --dry-run to perform the actual key rotation.")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Add flags
|
||||
rotateCmd.Flags().BoolVar(&dryRun, "dry-run", false, "Simulate key rotation without making changes")
|
||||
rotateCmd.Flags().IntVar(&batchSize, "batch-size", 100, "Number of records to process in each batch")
|
||||
rotateCmd.Flags().IntVar(&maxErrors, "max-errors", 50, "Maximum number of errors before aborting")
|
||||
rotateCmd.Flags().StringVar(&backupDir, "backup-dir", "", "Directory to store backup data (defaults to config backup_dir)")
|
||||
rotateCmd.Flags().BoolVar(&skipBackup, "skip-backup", false, "Skip database backup (not recommended)")
|
||||
rotateCmd.Flags().StringVar(&oldKeyEnvVar, "old-key-env", "", "Environment variable containing the old encryption key (defaults to GOMFT_ENCRYPTION_KEY)")
|
||||
rotateCmd.Flags().StringVar(&modelsFlag, "models", "auto", "Models to process (use 'auto' for automatic detection)")
|
||||
|
||||
return rotateCmd
|
||||
}
|
||||
|
||||
// keyManager is a simple implementation of the encryption.KeyManager interface
|
||||
// that uses a fixed key for the new encryption service
|
||||
type keyManager struct {
|
||||
key []byte
|
||||
}
|
||||
|
||||
func (km *keyManager) Initialize() error {
|
||||
// Already initialized with the key
|
||||
return nil
|
||||
}
|
||||
|
||||
func (km *keyManager) GetPrimaryKey() ([]byte, error) {
|
||||
return km.key, nil
|
||||
}
|
||||
|
||||
func (km *keyManager) GetEnvironmentVariableName() string {
|
||||
return "TEMP_KEY_MANAGER"
|
||||
}
|
||||
|
||||
func (km *keyManager) StoreKeyEnvironment(key []byte) error {
|
||||
// Not needed for this implementation
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
// generateSecureKey creates a cryptographically secure random key encoded as base64
|
||||
func generateSecureKey() (string, error) {
|
||||
return config.GenerateSecureKey()
|
||||
}
|
||||
|
||||
// updateEnvVar updates an environment variable in the .env file content
|
||||
func updateEnvVar(content, key, value string) (string, bool) {
|
||||
lines := strings.Split(content, "\n")
|
||||
prefix := key + "="
|
||||
updated := false
|
||||
|
||||
for i, line := range lines {
|
||||
if strings.HasPrefix(line, prefix) {
|
||||
lines[i] = prefix + value
|
||||
updated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n"), updated
|
||||
}
|
||||
|
||||
// copyFile copies a file from src to dst
|
||||
func copyFile(src, dst string) error {
|
||||
srcFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
dstFile, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
_, err = io.Copy(dstFile, srcFile)
|
||||
return err
|
||||
}
|
||||
Reference in new issue
Block a user