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.
This commit is contained in:
StarFleetCPTN
2025-04-16 17:18:53 -07:00
parent 88d0ac815a
commit 31871bd16e
59 changed files with 15842 additions and 594 deletions
@@ -0,0 +1,331 @@
package middleware
import (
"errors"
"fmt"
"reflect"
"strings"
"github.com/starfleetcptn/gomft/internal/encryption"
"gorm.io/gorm"
)
// EncryptionMiddleware handles automatic encryption and decryption of model fields
type EncryptionMiddleware struct {
encryptor *encryption.CredentialEncryptor
enabled bool
}
// NewEncryptionMiddleware creates a new middleware instance for encrypting/decrypting fields
func NewEncryptionMiddleware() (*EncryptionMiddleware, error) {
// Get the global credential encryptor
encryptor, err := encryption.GetGlobalCredentialEncryptor()
if err != nil {
return nil, fmt.Errorf("failed to initialize encryption middleware: %w", err)
}
return &EncryptionMiddleware{
encryptor: encryptor,
enabled: true,
}, nil
}
// Enable turns on automatic encryption/decryption
func (m *EncryptionMiddleware) Enable() {
m.enabled = true
}
// Disable turns off automatic encryption/decryption
func (m *EncryptionMiddleware) Disable() {
m.enabled = false
}
// IsEnabled returns whether the middleware is enabled
func (m *EncryptionMiddleware) IsEnabled() bool {
return m.enabled
}
// RegisterHooks registers the encryption/decryption hooks with the GORM instance
func (m *EncryptionMiddleware) RegisterHooks(db *gorm.DB) {
// Register BeforeSave hook to encrypt sensitive fields
db.Callback().Create().Before("gorm:create").Register("encrypt_before_create", m.encryptBeforeSave)
db.Callback().Update().Before("gorm:update").Register("encrypt_before_update", m.encryptBeforeSave)
// Register AfterFind hook to decrypt sensitive fields
db.Callback().Query().After("gorm:after_query").Register("decrypt_after_find", m.decryptAfterFind)
}
// encryptBeforeSave encrypts sensitive fields before saving to the database
func (m *EncryptionMiddleware) encryptBeforeSave(db *gorm.DB) {
if !m.enabled {
return
}
// Get the model value
value := db.Statement.ReflectValue
if value.Kind() == reflect.Ptr {
value = value.Elem()
}
// Skip if the value is not a struct
if value.Kind() != reflect.Struct {
return
}
// Process the model
if err := m.processModelForEncryption(value); err != nil {
db.AddError(fmt.Errorf("encryption middleware error: %w", err))
}
}
// decryptAfterFind decrypts sensitive fields after retrieving from the database
func (m *EncryptionMiddleware) decryptAfterFind(db *gorm.DB) {
if !m.enabled {
return
}
// Get the model value
value := db.Statement.ReflectValue
if value.Kind() == reflect.Ptr {
value = value.Elem()
}
// Handle slice of models
if value.Kind() == reflect.Slice {
for i := 0; i < value.Len(); i++ {
item := value.Index(i)
if item.Kind() == reflect.Ptr {
item = item.Elem()
}
if item.Kind() == reflect.Struct {
if err := m.processModelForDecryption(item); err != nil {
db.AddError(fmt.Errorf("decryption middleware error [index %d]: %w", i, err))
return
}
}
}
return
}
// Skip if the value is not a struct
if value.Kind() != reflect.Struct {
return
}
// Process the model
if err := m.processModelForDecryption(value); err != nil {
db.AddError(fmt.Errorf("decryption middleware error: %w", err))
}
}
// processModelForEncryption encrypts sensitive fields in a model
func (m *EncryptionMiddleware) processModelForEncryption(value reflect.Value) error {
modelType := value.Type()
// Special handling for StorageProvider type
if modelType.Name() == "StorageProvider" {
return m.encryptStorageProvider(value)
}
// Generic handling for models with encryptable fields
for i := 0; i < modelType.NumField(); i++ {
field := modelType.Field(i)
// Check if field requires encryption based on its name
fieldName := field.Name
if requiresEncryption, credType := encryption.RequiresEncryption(fieldName); requiresEncryption {
// Get the field value
fieldValue := value.Field(i)
if !fieldValue.CanInterface() || !fieldValue.CanSet() {
continue
}
// Get string value
strValue, ok := fieldValue.Interface().(string)
if !ok || strValue == "" {
continue
}
// If already encrypted, skip
if m.encryptor.IsEncrypted(strValue) {
continue
}
// Encrypt the field
encryptedValue, err := m.encryptor.Encrypt(strValue, credType)
if err != nil {
return fmt.Errorf("failed to encrypt field %s: %w", fieldName, err)
}
// Find the corresponding encrypted field
encryptedFieldName := "Encrypted" + fieldName
encryptedField := value.FieldByName(encryptedFieldName)
// If encrypted field exists and can be set, set it
if encryptedField.IsValid() && encryptedField.CanSet() {
encryptedField.SetString(encryptedValue)
// If the original field is marked with gorm:"-", we should clear it to prevent leaking it
if field.Tag.Get("gorm") == "-" {
fieldValue.SetString("")
}
}
}
}
return nil
}
// processModelForDecryption decrypts encrypted fields in a model
func (m *EncryptionMiddleware) processModelForDecryption(value reflect.Value) error {
modelType := value.Type()
// Special handling for StorageProvider type
if modelType.Name() == "StorageProvider" {
return m.decryptStorageProvider(value)
}
// Generic handling for models with encrypted fields
for i := 0; i < modelType.NumField(); i++ {
field := modelType.Field(i)
// Look for encrypted fields based on naming pattern
fieldName := field.Name
if strings.HasPrefix(fieldName, "Encrypted") {
originalFieldName := strings.TrimPrefix(fieldName, "Encrypted")
// Get the encrypted field value
encryptedFieldValue := value.Field(i)
if !encryptedFieldValue.CanInterface() {
continue
}
// Get encrypted string value
encryptedValue, ok := encryptedFieldValue.Interface().(string)
if !ok || encryptedValue == "" {
continue
}
// Decrypt the field
decryptedValue, err := m.encryptor.DecryptField(encryptedValue)
if err != nil {
// Log the error but continue
fmt.Printf("Warning: failed to decrypt field %s: %v\n", fieldName, err)
continue
}
// Find the corresponding original field
originalField := value.FieldByName(originalFieldName)
// If original field exists and can be set, set it
if originalField.IsValid() && originalField.CanSet() {
originalField.SetString(decryptedValue)
}
}
}
return nil
}
// encryptStorageProvider handles encryption for StorageProvider model fields
func (m *EncryptionMiddleware) encryptStorageProvider(value reflect.Value) error {
// Check if model implements GetSensitiveFields method
modelInterface := value.Addr().Interface()
// Type assertion to access the GetSensitiveFields method
model, ok := modelInterface.(interface {
GetSensitiveFields() map[string]string
})
if !ok {
return errors.New("StorageProvider model does not implement GetSensitiveFields")
}
// Get sensitive fields that need encryption
sensitiveFields := model.GetSensitiveFields()
// Encrypt each sensitive field
for fieldName, fieldValue := range sensitiveFields {
if fieldValue == "" {
continue
}
// Skip already encrypted values
if m.encryptor.IsEncrypted(fieldValue) {
continue
}
// Determine the credential type based on field name
_, credType := encryption.RequiresEncryption(fieldName)
// Encrypt the value
encryptedValue, err := m.encryptor.Encrypt(fieldValue, credType)
if err != nil {
return fmt.Errorf("failed to encrypt StorageProvider field %s: %w", fieldName, err)
}
// Find the corresponding encrypted field
encryptedFieldName := "Encrypted" + fieldName
encryptedField := value.FieldByName(encryptedFieldName)
// Set the encrypted value
if encryptedField.IsValid() && encryptedField.CanSet() {
encryptedField.SetString(encryptedValue)
// Clear the original field if it shouldn't be stored
originalField := value.FieldByName(fieldName)
if originalField.IsValid() && originalField.CanSet() {
// Find the field in the struct type to check its gorm tag
modelType := reflect.TypeOf(model).Elem()
if field, found := modelType.FieldByName(fieldName); found && field.Tag.Get("gorm") == "-" {
originalField.SetString("")
}
}
}
}
return nil
}
// decryptStorageProvider handles decryption for StorageProvider model fields
func (m *EncryptionMiddleware) decryptStorageProvider(value reflect.Value) error {
// Fields to decrypt
encryptedFields := []string{
"EncryptedPassword",
"EncryptedSecretKey",
"EncryptedClientSecret",
"EncryptedRefreshToken",
}
// Process each encrypted field
for _, fieldName := range encryptedFields {
encryptedField := value.FieldByName(fieldName)
if !encryptedField.IsValid() || !encryptedField.CanInterface() {
continue
}
// Get encrypted value
encryptedValue, ok := encryptedField.Interface().(string)
if !ok || encryptedValue == "" {
continue
}
// Decrypt value
decryptedValue, err := m.encryptor.DecryptField(encryptedValue)
if err != nil {
// Log warning but continue with other fields
fmt.Printf("Warning: failed to decrypt StorageProvider field %s: %v\n", fieldName, err)
continue
}
// Set decrypted value to the original field
originalFieldName := strings.TrimPrefix(fieldName, "Encrypted")
originalField := value.FieldByName(originalFieldName)
if originalField.IsValid() && originalField.CanSet() {
originalField.SetString(decryptedValue)
}
}
return nil
}
@@ -0,0 +1,253 @@
package middleware
import (
"testing"
"time"
"strings"
"github.com/glebarez/sqlite"
"github.com/starfleetcptn/gomft/internal/encryption"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
)
// TestModel is a simple model for testing encryption middleware
type TestModel struct {
ID uint `gorm:"primarykey"`
Name string `gorm:"not null"`
Password string `gorm:"-"` // Not stored in DB, only for form input
EncryptedPassword string `gorm:"column:encrypted_password"`
APIKey string `gorm:"-"` // Not stored in DB, only for form input
EncryptedAPIKey string `gorm:"column:encrypted_api_key"`
CreatedAt time.Time `gorm:"not null"`
UpdatedAt time.Time `gorm:"not null"`
}
// StorageProvider is a simplified version of the real model for testing
type StorageProvider struct {
ID uint `gorm:"primarykey"`
Name string `gorm:"not null"`
Type string `gorm:"not null"`
Password string `gorm:"-"` // Not stored in DB
EncryptedPassword string `gorm:"column:encrypted_password"`
SecretKey string `gorm:"-"` // Not stored in DB
EncryptedSecretKey string `gorm:"column:encrypted_secret_key"`
ClientSecret string `gorm:"-"` // Not stored in DB
EncryptedClientSecret string `gorm:"column:encrypted_client_secret"`
RefreshToken string `gorm:"-"` // Not stored in DB
EncryptedRefreshToken string `gorm:"column:encrypted_refresh_token"`
CreatedAt time.Time `gorm:"not null"`
UpdatedAt time.Time `gorm:"not null"`
}
// GetSensitiveFields returns a map of field names to values that need encryption
func (sp *StorageProvider) GetSensitiveFields() map[string]string {
sensitiveFields := make(map[string]string)
if sp.Password != "" {
sensitiveFields["Password"] = sp.Password
}
if sp.SecretKey != "" {
sensitiveFields["SecretKey"] = sp.SecretKey
}
if sp.ClientSecret != "" {
sensitiveFields["ClientSecret"] = sp.ClientSecret
}
if sp.RefreshToken != "" {
sensitiveFields["RefreshToken"] = sp.RefreshToken
}
return sensitiveFields
}
func setupTestDB(t *testing.T) *gorm.DB {
// Initialize in-memory SQLite database
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
require.NoError(t, err, "Failed to connect to in-memory database")
// Migrate the test models
err = db.AutoMigrate(&TestModel{}, &StorageProvider{})
require.NoError(t, err, "Failed to migrate test models")
return db
}
func setupEncryptionMiddleware(t *testing.T) (*EncryptionMiddleware, error) {
// Initialize the encryption key manager for testing
err := encryption.InitializeKeyManager("test-key")
require.NoError(t, err, "Failed to initialize key manager")
// Create the encryption middleware
return NewEncryptionMiddleware()
}
func TestEncryptionMiddlewareWithGenericModel(t *testing.T) {
// Setup
db := setupTestDB(t)
middleware, err := setupEncryptionMiddleware(t)
require.NoError(t, err, "Failed to setup encryption middleware")
// Register hooks with GORM
middleware.RegisterHooks(db)
// Create a test model
testModel := &TestModel{
Name: "Test User",
Password: "securePassword123",
APIKey: "api-key-12345",
}
// Save the model - should trigger encryption
err = db.Create(testModel).Error
require.NoError(t, err, "Failed to save test model")
// Verify encrypted fields are set and original fields are cleared
assert.Empty(t, testModel.Password, "Password should be cleared after save")
assert.Empty(t, testModel.APIKey, "APIKey should be cleared after save")
assert.NotEmpty(t, testModel.EncryptedPassword, "EncryptedPassword should be set")
assert.NotEmpty(t, testModel.EncryptedAPIKey, "EncryptedAPIKey should be set")
assert.True(t, strings.HasPrefix(testModel.EncryptedPassword, encryption.EncryptedPrefix), "EncryptedPassword should have encryption prefix")
assert.True(t, strings.HasPrefix(testModel.EncryptedAPIKey, encryption.EncryptedPrefix), "EncryptedAPIKey should have encryption prefix")
// Test retrieval and automatic decryption
retrievedModel := new(TestModel)
err = db.First(retrievedModel, testModel.ID).Error
require.NoError(t, err, "Failed to retrieve test model")
// Verify decryption
assert.Equal(t, "securePassword123", retrievedModel.Password, "Password should be automatically decrypted")
assert.Equal(t, "api-key-12345", retrievedModel.APIKey, "APIKey should be automatically decrypted")
assert.NotEmpty(t, retrievedModel.EncryptedPassword, "EncryptedPassword should remain set")
assert.NotEmpty(t, retrievedModel.EncryptedAPIKey, "EncryptedAPIKey should remain set")
}
func TestEncryptionMiddlewareWithStorageProvider(t *testing.T) {
// Setup
db := setupTestDB(t)
middleware, err := setupEncryptionMiddleware(t)
require.NoError(t, err, "Failed to setup encryption middleware")
// Register hooks with GORM
middleware.RegisterHooks(db)
// Create a storage provider
provider := &StorageProvider{
Name: "Test S3",
Type: "s3",
Password: "testPassword",
SecretKey: "testSecretKey",
ClientSecret: "testClientSecret",
RefreshToken: "testRefreshToken",
}
// Save the provider - should trigger encryption
err = db.Create(provider).Error
require.NoError(t, err, "Failed to save storage provider")
// Verify encrypted fields are set and original fields are cleared
assert.Empty(t, provider.Password, "Password should be cleared after save")
assert.Empty(t, provider.SecretKey, "SecretKey should be cleared after save")
assert.Empty(t, provider.ClientSecret, "ClientSecret should be cleared after save")
assert.Empty(t, provider.RefreshToken, "RefreshToken should be cleared after save")
assert.NotEmpty(t, provider.EncryptedPassword, "EncryptedPassword should be set")
assert.NotEmpty(t, provider.EncryptedSecretKey, "EncryptedSecretKey should be set")
assert.NotEmpty(t, provider.EncryptedClientSecret, "EncryptedClientSecret should be set")
assert.NotEmpty(t, provider.EncryptedRefreshToken, "EncryptedRefreshToken should be set")
// Test retrieval and automatic decryption
retrievedProvider := new(StorageProvider)
err = db.First(retrievedProvider, provider.ID).Error
require.NoError(t, err, "Failed to retrieve storage provider")
// Verify decryption
assert.Equal(t, "testPassword", retrievedProvider.Password, "Password should be automatically decrypted")
assert.Equal(t, "testSecretKey", retrievedProvider.SecretKey, "SecretKey should be automatically decrypted")
assert.Equal(t, "testClientSecret", retrievedProvider.ClientSecret, "ClientSecret should be automatically decrypted")
assert.Equal(t, "testRefreshToken", retrievedProvider.RefreshToken, "RefreshToken should be automatically decrypted")
}
func TestEncryptionMiddlewareWithMultipleRecords(t *testing.T) {
// Setup
db := setupTestDB(t)
middleware, err := setupEncryptionMiddleware(t)
require.NoError(t, err, "Failed to setup encryption middleware")
// Register hooks with GORM
middleware.RegisterHooks(db)
// Create multiple test models
models := []TestModel{
{Name: "User 1", Password: "password1", APIKey: "apikey1"},
{Name: "User 2", Password: "password2", APIKey: "apikey2"},
{Name: "User 3", Password: "password3", APIKey: "apikey3"},
}
// Save all models
err = db.Create(&models).Error
require.NoError(t, err, "Failed to save multiple test models")
// Retrieve all models
var retrievedModels []TestModel
err = db.Find(&retrievedModels).Error
require.NoError(t, err, "Failed to retrieve all test models")
// Verify count
assert.Equal(t, 3, len(retrievedModels), "Should retrieve 3 models")
// Verify each model was properly decrypted
expectedPasswords := []string{"password1", "password2", "password3"}
expectedAPIKeys := []string{"apikey1", "apikey2", "apikey3"}
for i, model := range retrievedModels {
assert.Equal(t, expectedPasswords[i], model.Password, "Password should be automatically decrypted")
assert.Equal(t, expectedAPIKeys[i], model.APIKey, "APIKey should be automatically decrypted")
assert.NotEmpty(t, model.EncryptedPassword, "EncryptedPassword should remain set")
assert.NotEmpty(t, model.EncryptedAPIKey, "EncryptedAPIKey should remain set")
}
}
func TestEncryptionMiddlewareDisabled(t *testing.T) {
// Setup
db := setupTestDB(t)
middleware, err := setupEncryptionMiddleware(t)
require.NoError(t, err, "Failed to setup encryption middleware")
// Register hooks with GORM
middleware.RegisterHooks(db)
// Disable the middleware
middleware.Disable()
assert.False(t, middleware.IsEnabled(), "Middleware should be disabled")
// Create a test model
testModel := &TestModel{
Name: "Test User",
Password: "securePassword123",
APIKey: "api-key-12345",
}
// Save the model - should NOT trigger encryption since middleware is disabled
err = db.Create(testModel).Error
require.NoError(t, err, "Failed to save test model")
// Verify sensitive fields are NOT encrypted
assert.Equal(t, "securePassword123", testModel.Password, "Password should not be cleared when middleware is disabled")
assert.Equal(t, "api-key-12345", testModel.APIKey, "APIKey should not be cleared when middleware is disabled")
assert.Empty(t, testModel.EncryptedPassword, "EncryptedPassword should not be set when middleware is disabled")
assert.Empty(t, testModel.EncryptedAPIKey, "EncryptedAPIKey should not be set when middleware is disabled")
// Re-enable the middleware for subsequent operations
middleware.Enable()
assert.True(t, middleware.IsEnabled(), "Middleware should be enabled")
// Update the model - should now trigger encryption
testModel.Password = "newPassword456"
err = db.Save(testModel).Error
require.NoError(t, err, "Failed to update test model")
// Verify encryption now happened
assert.Empty(t, testModel.Password, "Password should be cleared after update with middleware enabled")
assert.NotEmpty(t, testModel.EncryptedPassword, "EncryptedPassword should be set after update with middleware enabled")
}