Files
StarFleetCPTN 31871bd16e feat: Implement storage provider management functionality
- Added new routes and handlers for managing storage providers, including creation, editing, and deletion.
- Introduced a new StorageProvider form component for user input.
- Enhanced the database schema to support storage provider references in transfer configurations.
- Implemented encryption for sensitive fields in storage provider data.
- Added tests for storage provider API endpoints and integration with the database.
- Updated frontend components to support storage provider selection and testing.
2025-04-16 17:18:53 -07:00

513 lines
14 KiB
Go

package keyrotation
import (
"bytes"
"encoding/base64"
"fmt"
"os"
"reflect"
"strings"
"testing"
"github.com/glebarez/sqlite"
"github.com/starfleetcptn/gomft/internal/encryption"
"github.com/starfleetcptn/gomft/internal/encryption/audit"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
)
// TestModel is a simple model with encrypted fields for testing
type TestModel struct {
ID uint `gorm:"primaryKey"`
Name string
EncryptedField string
EncryptedData string
EncryptedKey string
StandardField string
}
// setupTestAuditor creates an auditor for testing with buffer for capturing logs
func setupTestAuditor(t testing.TB) (*audit.SecurityAuditor, *bytes.Buffer) {
logBuffer := new(bytes.Buffer)
errorBuffer := new(bytes.Buffer)
auditor, err := audit.New()
if err != nil {
t.Fatal(err)
}
// Set log writers to capture output
auditValue := reflect.ValueOf(auditor).Elem()
if logField := auditValue.FieldByName("logWriter"); logField.IsValid() && logField.CanSet() {
logField.Set(reflect.ValueOf(logBuffer))
}
if errorField := auditValue.FieldByName("errorWriter"); errorField.IsValid() && errorField.CanSet() {
errorField.Set(reflect.ValueOf(errorBuffer))
}
return auditor, logBuffer
}
// setupTestDB creates a test database with the TestModel
func setupTestDB(t *testing.T) *gorm.DB {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
// Migrate the schema
err = db.AutoMigrate(&TestModel{})
require.NoError(t, err)
return db
}
// setupEncryptionServices creates old and new encryption services for testing
func setupEncryptionServices(t testing.TB) (*encryption.EncryptionService, *encryption.EncryptionService) {
// Setup old key
oldKeyEnv := "TEST_OLD_KEY"
oldKey := make([]byte, encryption.AES256KeySize)
for i := range oldKey {
oldKey[i] = byte(i % 256)
}
os.Setenv(oldKeyEnv, base64.StdEncoding.EncodeToString(oldKey))
// Setup new key
newKeyEnv := "TEST_NEW_KEY"
newKey := make([]byte, encryption.AES256KeySize)
for i := range newKey {
newKey[i] = byte((i + 128) % 256) // Different key
}
os.Setenv(newKeyEnv, base64.StdEncoding.EncodeToString(newKey))
if t, ok := t.(*testing.T); ok {
t.Cleanup(func() {
os.Unsetenv(oldKeyEnv)
os.Unsetenv(newKeyEnv)
})
}
// Create key managers
oldKM := encryption.NewKeyManager(oldKeyEnv)
err := oldKM.Initialize()
if err != nil {
t.Fatal(err)
}
newKM := encryption.NewKeyManager(newKeyEnv)
err = newKM.Initialize()
if err != nil {
t.Fatal(err)
}
// Create encryption services
oldService, err := encryption.NewEncryptionService(oldKM)
if err != nil {
t.Fatal(err)
}
newService, err := encryption.NewEncryptionService(newKM)
if err != nil {
t.Fatal(err)
}
return oldService, newService
}
// createTestData creates test records with encrypted fields
func createTestData(t testing.TB, db *gorm.DB, oldService *encryption.EncryptionService, count int) {
for i := 1; i <= count; i++ {
// Create encrypted values with the old key
field1, err := oldService.EncryptString(fmt.Sprintf("secret-field-%d", i))
if err != nil {
t.Fatal(err)
}
field2, err := oldService.EncryptString(fmt.Sprintf("secret-data-%d", i))
if err != nil {
t.Fatal(err)
}
field3, err := oldService.EncryptString(fmt.Sprintf("secret-key-%d", i))
if err != nil {
t.Fatal(err)
}
// Create a test record
record := TestModel{
Name: fmt.Sprintf("Test Record %d", i),
EncryptedField: encryption.EncryptedPrefix + field1,
EncryptedData: encryption.EncryptedPrefix + field2,
EncryptedKey: encryption.EncryptedPrefix + field3,
StandardField: fmt.Sprintf("standard-field-%d", i),
}
// Save to DB
result := db.Create(&record)
if err := result.Error; err != nil {
t.Fatal(err)
}
}
}
func TestNewKeyRotator(t *testing.T) {
db := setupTestDB(t)
oldService, newService := setupEncryptionServices(t)
auditor, _ := setupTestAuditor(t)
t.Run("Valid rotator creation", func(t *testing.T) {
rotator, err := NewKeyRotator(db, oldService, newService, auditor)
require.NoError(t, err)
assert.NotNil(t, rotator)
assert.False(t, rotator.dryRun)
assert.Equal(t, 100, rotator.batchSize)
assert.Equal(t, 50, rotator.maxErrors)
})
t.Run("Nil DB", func(t *testing.T) {
rotator, err := NewKeyRotator(nil, oldService, newService, auditor)
require.Error(t, err)
assert.Nil(t, rotator)
assert.Equal(t, ErrNilDB, err)
})
t.Run("Nil old service", func(t *testing.T) {
rotator, err := NewKeyRotator(db, nil, newService, auditor)
require.Error(t, err)
assert.Nil(t, rotator)
assert.Equal(t, ErrNoOldKey, err)
})
t.Run("Nil new service", func(t *testing.T) {
rotator, err := NewKeyRotator(db, oldService, nil, auditor)
require.Error(t, err)
assert.Nil(t, rotator)
assert.Equal(t, ErrNoNewKey, err)
})
t.Run("Same service", func(t *testing.T) {
rotator, err := NewKeyRotator(db, oldService, oldService, auditor)
require.Error(t, err)
assert.Nil(t, rotator)
assert.Equal(t, ErrSameKey, err)
})
t.Run("Default auditor", func(t *testing.T) {
rotator, err := NewKeyRotator(db, oldService, newService, nil)
require.NoError(t, err)
assert.NotNil(t, rotator)
assert.NotNil(t, rotator.auditor)
})
}
func TestKeyRotatorConfigMethods(t *testing.T) {
db := setupTestDB(t)
oldService, newService := setupEncryptionServices(t)
auditor, _ := setupTestAuditor(t)
rotator, err := NewKeyRotator(db, oldService, newService, auditor)
require.NoError(t, err)
t.Run("SetDryRun", func(t *testing.T) {
rotator.SetDryRun(true)
assert.True(t, rotator.dryRun)
rotator.SetDryRun(false)
assert.False(t, rotator.dryRun)
})
t.Run("SetBatchSize", func(t *testing.T) {
rotator.SetBatchSize(200)
assert.Equal(t, 200, rotator.batchSize)
// Test with invalid value
rotator.SetBatchSize(0)
assert.Equal(t, 200, rotator.batchSize) // Shouldn't change
rotator.SetBatchSize(-10)
assert.Equal(t, 200, rotator.batchSize) // Shouldn't change
})
t.Run("SetMaxErrors", func(t *testing.T) {
rotator.SetMaxErrors(100)
assert.Equal(t, 100, rotator.maxErrors)
rotator.SetMaxErrors(0)
assert.Equal(t, 0, rotator.maxErrors) // 0 is valid (no max)
// Test with invalid value
rotator.SetMaxErrors(-10)
assert.Equal(t, 0, rotator.maxErrors) // Shouldn't change
})
}
func TestRotateKeys(t *testing.T) {
db := setupTestDB(t)
oldService, newService := setupEncryptionServices(t)
auditor, logBuffer := setupTestAuditor(t)
rotator, err := NewKeyRotator(db, oldService, newService, auditor)
require.NoError(t, err)
t.Run("Rotate keys for model with no records", func(t *testing.T) {
stats, err := rotator.RotateKeys(&TestModel{}, "")
require.Error(t, err)
assert.Equal(t, ErrNoDataToMigrate, err)
assert.Equal(t, 0, stats.TotalRecords)
})
t.Run("Rotate keys for non-struct model", func(t *testing.T) {
stats, err := rotator.RotateKeys("not a struct", "")
require.Error(t, err)
assert.Contains(t, err.Error(), "must be a struct")
assert.Equal(t, 0, stats.TotalRecords, "Expected total records to be 0 for non-struct model")
})
t.Run("Rotate keys for model with records", func(t *testing.T) {
// Reset log buffer
logBuffer.Reset()
// Create test data
createTestData(t, db, oldService, 10)
// Perform key rotation
stats, err := rotator.RotateKeys(&TestModel{}, "")
require.NoError(t, err)
assert.Equal(t, 10, stats.TotalRecords)
assert.Equal(t, 10, stats.ProcessedRecords)
assert.Equal(t, 0, stats.FailedRecords)
assert.NotZero(t, stats.ElapsedTime)
assert.Empty(t, stats.Errors)
// Verify that records were updated with re-encrypted values
var records []TestModel
result := db.Find(&records)
require.NoError(t, result.Error)
assert.Equal(t, 10, len(records))
// Test a sample record to ensure it was re-encrypted properly
record := records[0]
// Verify the old key can't decrypt the new values
_, err = oldService.DecryptString(strings.TrimPrefix(record.EncryptedField, encryption.EncryptedPrefix))
assert.Error(t, err, "Old key should not be able to decrypt new values")
// Verify the new key can decrypt the values
decryptedField, err := newService.DecryptString(strings.TrimPrefix(record.EncryptedField, encryption.EncryptedPrefix))
require.NoError(t, err)
assert.Equal(t, "secret-field-1", decryptedField)
// Verify audit logs were created
logContent := logBuffer.String()
assert.Contains(t, logContent, "key_rotation")
assert.Contains(t, logContent, "TestModel")
// Verify no sensitive data in logs
assert.NotContains(t, logContent, "secret-field")
assert.NotContains(t, logContent, "secret-data")
assert.NotContains(t, logContent, "secret-key")
})
t.Run("Dry run mode", func(t *testing.T) {
// Reset the database
db.Exec("DELETE FROM test_models")
createTestData(t, db, oldService, 5)
// Create a new rotator with dry run enabled
rotator, err := NewKeyRotator(db, oldService, newService, auditor)
require.NoError(t, err)
rotator.SetDryRun(true)
// Perform key rotation
stats, err := rotator.RotateKeys(&TestModel{}, "")
require.NoError(t, err)
assert.Equal(t, 5, stats.TotalRecords)
// Verify that records were NOT updated with re-encrypted values
var records []TestModel
result := db.Find(&records)
require.NoError(t, result.Error)
// Test a sample record to ensure it was NOT re-encrypted
record := records[0]
// Verify the old key CAN decrypt the values (because they weren't changed)
decryptedField, err := oldService.DecryptString(strings.TrimPrefix(record.EncryptedField, encryption.EncryptedPrefix))
require.NoError(t, err)
assert.Equal(t, "secret-field-1", decryptedField)
})
}
func TestRotateKeysWithErrors(t *testing.T) {
db := setupTestDB(t)
oldService, newService := setupEncryptionServices(t)
auditor, _ := setupTestAuditor(t)
rotator, err := NewKeyRotator(db, oldService, newService, auditor)
require.NoError(t, err)
// Create test data with one corrupted record
createTestData(t, db, oldService, 5)
// Create a corrupted record that can't be decrypted
corruptedRecord := TestModel{
Name: "Corrupted Record",
EncryptedField: encryption.EncryptedPrefix + "corrupted-data",
EncryptedData: encryption.EncryptedPrefix + "corrupted-data",
StandardField: "standard-field",
}
result := db.Create(&corruptedRecord)
require.NoError(t, result.Error)
// Perform key rotation
stats, err := rotator.RotateKeys(&TestModel{}, "")
require.NoError(t, err) // Should still succeed overall
assert.Equal(t, 6, stats.TotalRecords)
assert.Equal(t, 5, stats.ProcessedRecords) // Only 5 should be processed successfully
assert.Equal(t, 0, stats.FailedRecords) // Failure to decrypt is skipped, not counted as error
// Verify that the valid records were updated
var records []TestModel
db.Where("name LIKE ?", "Test Record%").Find(&records)
require.Equal(t, 5, len(records))
for _, record := range records {
// Verify the new key can decrypt
_, err = newService.DecryptString(strings.TrimPrefix(record.EncryptedField, encryption.EncryptedPrefix))
assert.NoError(t, err)
}
// Verify the corrupted record wasn't changed
var corrupted TestModel
db.Where("name = ?", "Corrupted Record").First(&corrupted)
assert.Equal(t, encryption.EncryptedPrefix+"corrupted-data", corrupted.EncryptedField)
}
func TestGetPrimaryKeyValue(t *testing.T) {
type TestStruct struct {
ID uint
CustomID string
NotAnID string
OtherData string
}
t.Run("Default ID field", func(t *testing.T) {
test := TestStruct{ID: 123, OtherData: "test"}
val := getPrimaryKeyValue(reflect.ValueOf(test), "")
assert.Equal(t, uint(123), val)
})
t.Run("Custom ID field", func(t *testing.T) {
test := TestStruct{ID: 123, CustomID: "ABC123", OtherData: "test"}
val := getPrimaryKeyValue(reflect.ValueOf(test), "CustomID")
assert.Equal(t, "ABC123", val)
})
t.Run("Non-existent ID field", func(t *testing.T) {
test := TestStruct{ID: 123, OtherData: "test"}
val := getPrimaryKeyValue(reflect.ValueOf(test), "NonExistentID")
assert.Equal(t, "<unknown>", val)
})
}
// BenchmarkKeyRotation measures the performance of key rotation
func BenchmarkKeyRotation(b *testing.B) {
// Setup
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
b.Fatal(err)
}
db.AutoMigrate(&TestModel{})
oldService, newService := setupEncryptionServices(b)
auditor, _ := setupTestAuditor(b)
rotator, err := NewKeyRotator(db, oldService, newService, auditor)
if err != nil {
b.Fatal(err)
}
// Create benchmark data sets of different sizes
benchmarks := []struct {
name string
numRecords int
}{
{"Small (10 records)", 10},
{"Medium (100 records)", 100},
{"Large (500 records)", 500},
}
for _, bm := range benchmarks {
b.Run(bm.name, func(b *testing.B) {
// Reset the database for each benchmark iteration
db.Exec("DELETE FROM test_models")
createTestData(b, db, oldService, bm.numRecords)
b.ResetTimer()
for i := 0; i < b.N; i++ {
stats, err := rotator.RotateKeys(&TestModel{}, "")
if err != nil {
b.Fatal(err)
}
if stats.ProcessedRecords != bm.numRecords {
b.Fatalf("Expected %d records, got %d", bm.numRecords, stats.ProcessedRecords)
}
// Reset for the next iteration
if i < b.N-1 {
db.Exec("DELETE FROM test_models")
createTestData(b, db, oldService, bm.numRecords)
}
}
})
}
}
// Benchmarks for different batch sizes
func BenchmarkBatchSizes(b *testing.B) {
// Setup
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
b.Fatal(err)
}
db.AutoMigrate(&TestModel{})
oldService, newService := setupEncryptionServices(b)
auditor, _ := setupTestAuditor(b)
// Create a dataset of 500 records
const numRecords = 500
createTestData(b, db, oldService, numRecords)
// Test different batch sizes
batchSizes := []int{10, 50, 100, 200, 500}
for _, batchSize := range batchSizes {
b.Run(fmt.Sprintf("BatchSize_%d", batchSize), func(b *testing.B) {
rotator, err := NewKeyRotator(db, oldService, newService, auditor)
if err != nil {
b.Fatal(err)
}
rotator.SetBatchSize(batchSize)
b.ResetTimer()
for i := 0; i < b.N; i++ {
// Reset data before each run
if i > 0 {
db.Exec("DELETE FROM test_models")
createTestData(b, db, oldService, numRecords)
}
stats, err := rotator.RotateKeys(&TestModel{}, "")
if err != nil {
b.Fatal(err)
}
if stats.ProcessedRecords != numRecords {
b.Fatalf("Expected %d records, got %d", numRecords, stats.ProcessedRecords)
}
}
})
}
}