mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-09-08 23:50:48 +02:00
773 lines
24 KiB
Go
773 lines
24 KiB
Go
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
|
|
}
|