1398 lines
60 KiB
Go
1398 lines
60 KiB
Go
package configstore
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
bifrost "github.com/maximhq/bifrost/core"
|
|
"github.com/maximhq/bifrost/core/schemas"
|
|
"github.com/maximhq/bifrost/framework/configstore/tables"
|
|
"github.com/maximhq/bifrost/framework/encrypt"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/logger"
|
|
)
|
|
|
|
const testEncryptionKey = "test-encryption-key-for-testing-32bytes"
|
|
|
|
func init() {
|
|
encrypt.Init(testEncryptionKey, bifrost.NewDefaultLogger(schemas.LogLevelInfo))
|
|
}
|
|
|
|
// setupEncryptionTestStore creates an in-memory SQLite database with all tables
|
|
// migrated and returns an RDBConfigStore for testing the startup encryption pass.
|
|
func setupEncryptionTestStore(t *testing.T) (*RDBConfigStore, *gorm.DB) {
|
|
t.Helper()
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
|
Logger: logger.Default.LogMode(logger.Silent),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = db.AutoMigrate(
|
|
&tables.TableKey{},
|
|
&tables.TableProvider{},
|
|
&tables.TableMCPClient{},
|
|
&tables.TablePlugin{},
|
|
&tables.TableVirtualKey{},
|
|
&tables.SessionsTable{},
|
|
&tables.TableOauthConfig{},
|
|
&tables.TableOauthToken{},
|
|
&tables.TableVectorStoreConfig{},
|
|
&tables.TableBudget{},
|
|
&tables.TableRateLimit{},
|
|
&tables.TableVirtualKeyProviderConfig{},
|
|
&tables.TableVirtualKeyProviderConfigKey{},
|
|
&tables.TableCustomer{},
|
|
&tables.TableTeam{},
|
|
&tables.TableClientConfig{},
|
|
&tables.TableVirtualKeyMCPConfig{},
|
|
&tables.TableModel{},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
store := &RDBConfigStore{logger: bifrost.NewDefaultLogger(schemas.LogLevelInfo)}
|
|
store.db.Store(db)
|
|
store.migrateOnFreshFn = func(ctx context.Context, fn func(context.Context, *gorm.DB) error) error {
|
|
return fn(ctx, store.DB())
|
|
}
|
|
store.refreshPoolFn = func(ctx context.Context) error { return nil }
|
|
return store, db
|
|
}
|
|
|
|
// insertPlaintextRow inserts a row directly into the DB via raw SQL, bypassing GORM hooks,
|
|
// so the row has encryption_status='plain_text' and plaintext sensitive data.
|
|
func insertPlaintextRow(t *testing.T, db *gorm.DB, sql string, args ...any) {
|
|
t.Helper()
|
|
require.NoError(t, db.Exec(sql, args...).Error)
|
|
}
|
|
|
|
// ============================================================================
|
|
// EncryptPlaintextRows — full startup pass
|
|
// ============================================================================
|
|
|
|
func TestEncryptPlaintextRows_EncryptsAllTables(t *testing.T) {
|
|
store, db := setupEncryptionTestStore(t)
|
|
ctx := context.Background()
|
|
now := time.Now().UTC().Format("2006-01-02 15:04:05")
|
|
future := time.Now().Add(time.Hour).UTC().Format("2006-01-02 15:04:05")
|
|
|
|
// Insert plaintext rows across all tables (bypassing hooks)
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO config_keys (name, provider_id, provider, key_id, value, encryption_status, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, 'plain_text', ?, ?)`,
|
|
"test-key", 1, "openai", "key-1", "sk-plaintext-key", now, now)
|
|
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO governance_virtual_keys (id, name, value, is_active, encryption_status, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, 'plain_text', ?, ?)`,
|
|
"vk-1", "test-vk", "vk-plaintext-value", true, now, now)
|
|
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO sessions (token, encryption_status, expires_at, created_at, updated_at)
|
|
VALUES (?, 'plain_text', ?, ?, ?)`,
|
|
"session-plaintext-token", future, now, now)
|
|
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO oauth_tokens (id, access_token, refresh_token, token_type, encryption_status, expires_at, created_at, updated_at)
|
|
VALUES (?, ?, ?, 'Bearer', 'plain_text', ?, ?, ?)`,
|
|
"tok-1", "plaintext-access-token", "plaintext-refresh-token", future, now, now)
|
|
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO oauth_configs (id, client_secret, code_verifier, redirect_uri, state, status, encryption_status, created_at, updated_at, expires_at)
|
|
VALUES (?, ?, ?, ?, ?, 'pending', 'plain_text', ?, ?, ?)`,
|
|
"cfg-1", "plaintext-client-secret", "plaintext-verifier", "https://example.com/cb", "csrf-state", now, now, future)
|
|
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO config_mcp_clients (client_id, name, connection_type, connection_string, headers_json, encryption_status, created_at, updated_at)
|
|
VALUES (?, ?, 'sse', ?, ?, 'plain_text', ?, ?)`,
|
|
"mcp-1", "test-mcp", "https://mcp.example.com", `{"Authorization":"Bearer token"}`, now, now)
|
|
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO config_providers (name, proxy_config_json, encryption_status, created_at, updated_at)
|
|
VALUES (?, ?, 'plain_text', ?, ?)`,
|
|
"openai", `{"url":"https://proxy.example.com"}`, now, now)
|
|
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO config_vector_store (enabled, type, config, encryption_status, created_at, updated_at)
|
|
VALUES (?, ?, ?, 'plain_text', ?, ?)`,
|
|
true, "redis", `{"host":"redis.example.com","password":"secret"}`, now, now)
|
|
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO config_plugins (name, enabled, version, config_json, encryption_status, created_at, updated_at)
|
|
VALUES (?, ?, 1, ?, 'plain_text', ?, ?)`,
|
|
"test-plugin", true, `{"api_key":"plugin-secret"}`, now, now)
|
|
|
|
// Run the startup encryption pass
|
|
err := store.EncryptPlaintextRows(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// Verify all rows are now encrypted in raw DB
|
|
var keyRow map[string]any
|
|
db.Table("config_keys").Where("name = ?", "test-key").Take(&keyRow)
|
|
assert.Equal(t, "encrypted", keyRow["encryption_status"])
|
|
assert.NotEqual(t, "sk-plaintext-key", keyRow["value"])
|
|
|
|
var vkRow map[string]any
|
|
db.Table("governance_virtual_keys").Where("id = ?", "vk-1").Take(&vkRow)
|
|
assert.Equal(t, "encrypted", vkRow["encryption_status"])
|
|
assert.NotEqual(t, "vk-plaintext-value", vkRow["value"])
|
|
|
|
var sessionRow map[string]any
|
|
db.Table("sessions").Take(&sessionRow)
|
|
assert.Equal(t, "encrypted", sessionRow["encryption_status"])
|
|
assert.NotEqual(t, "session-plaintext-token", sessionRow["token"])
|
|
|
|
var tokRow map[string]any
|
|
db.Table("oauth_tokens").Where("id = ?", "tok-1").Take(&tokRow)
|
|
assert.Equal(t, "encrypted", tokRow["encryption_status"])
|
|
assert.NotEqual(t, "plaintext-access-token", tokRow["access_token"])
|
|
|
|
var cfgRow map[string]any
|
|
db.Table("oauth_configs").Where("id = ?", "cfg-1").Take(&cfgRow)
|
|
assert.Equal(t, "encrypted", cfgRow["encryption_status"])
|
|
assert.NotEqual(t, "plaintext-client-secret", cfgRow["client_secret"])
|
|
|
|
var mcpRow map[string]any
|
|
db.Table("config_mcp_clients").Where("client_id = ?", "mcp-1").Take(&mcpRow)
|
|
assert.Equal(t, "encrypted", mcpRow["encryption_status"])
|
|
|
|
var providerRow map[string]any
|
|
db.Table("config_providers").Where("name = ?", "openai").Take(&providerRow)
|
|
assert.Equal(t, "encrypted", providerRow["encryption_status"])
|
|
|
|
var vsRow map[string]any
|
|
db.Table("config_vector_store").Take(&vsRow)
|
|
assert.Equal(t, "encrypted", vsRow["encryption_status"])
|
|
|
|
var pluginRow map[string]any
|
|
db.Table("config_plugins").Where("name = ?", "test-plugin").Take(&pluginRow)
|
|
assert.Equal(t, "encrypted", pluginRow["encryption_status"])
|
|
}
|
|
|
|
func TestEncryptPlaintextRows_SkipsAlreadyEncrypted(t *testing.T) {
|
|
store, db := setupEncryptionTestStore(t)
|
|
ctx := context.Background()
|
|
|
|
// Create a key through the normal GORM path (which encrypts via hooks)
|
|
key := &tables.TableKey{
|
|
Name: "already-encrypted",
|
|
ProviderID: 1,
|
|
Provider: "openai",
|
|
KeyID: "enc-key-1",
|
|
Value: *schemas.NewEnvVar("sk-secret"),
|
|
}
|
|
require.NoError(t, db.Create(key).Error)
|
|
|
|
// Grab the encrypted value from DB
|
|
var rawBefore map[string]any
|
|
db.Table("config_keys").Where("id = ?", key.ID).Take(&rawBefore)
|
|
encryptedBefore := rawBefore["value"]
|
|
|
|
// Run the startup pass
|
|
err := store.EncryptPlaintextRows(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// The encrypted value should not have changed (not double-encrypted)
|
|
var rawAfter map[string]any
|
|
db.Table("config_keys").Where("id = ?", key.ID).Take(&rawAfter)
|
|
assert.Equal(t, encryptedBefore, rawAfter["value"])
|
|
}
|
|
|
|
func TestEncryptPlaintextRows_Idempotent(t *testing.T) {
|
|
store, db := setupEncryptionTestStore(t)
|
|
ctx := context.Background()
|
|
now := time.Now().UTC().Format("2006-01-02 15:04:05")
|
|
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO config_keys (name, provider_id, provider, key_id, value, encryption_status, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, 'plain_text', ?, ?)`,
|
|
"idempotent-key", 1, "openai", "idem-1", "sk-plaintext", now, now)
|
|
|
|
// Run twice
|
|
err := store.EncryptPlaintextRows(ctx)
|
|
require.NoError(t, err)
|
|
|
|
err = store.EncryptPlaintextRows(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// Should still be readable via GORM hooks
|
|
var found tables.TableKey
|
|
require.NoError(t, db.Where("name = ?", "idempotent-key").First(&found).Error)
|
|
assert.Equal(t, "sk-plaintext", found.Value.GetValue())
|
|
}
|
|
|
|
func TestEncryptPlaintextRows_HandlesNullEncryptionStatus(t *testing.T) {
|
|
store, db := setupEncryptionTestStore(t)
|
|
ctx := context.Background()
|
|
now := time.Now().UTC().Format("2006-01-02 15:04:05")
|
|
|
|
// Insert with NULL encryption_status (legacy row)
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO config_keys (name, provider_id, provider, key_id, value, encryption_status, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, NULL, ?, ?)`,
|
|
"null-status-key", 1, "openai", "null-1", "sk-null-status", now, now)
|
|
|
|
// Insert with empty encryption_status
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO config_keys (name, provider_id, provider, key_id, value, encryption_status, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, '', ?, ?)`,
|
|
"empty-status-key", 1, "openai", "empty-1", "sk-empty-status", now, now)
|
|
|
|
err := store.EncryptPlaintextRows(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// Both should be encrypted now
|
|
var row1 map[string]any
|
|
db.Table("config_keys").Where("name = ?", "null-status-key").Take(&row1)
|
|
assert.Equal(t, "encrypted", row1["encryption_status"])
|
|
|
|
var row2 map[string]any
|
|
db.Table("config_keys").Where("name = ?", "empty-status-key").Take(&row2)
|
|
assert.Equal(t, "encrypted", row2["encryption_status"])
|
|
}
|
|
|
|
// ============================================================================
|
|
// Individual batch functions
|
|
// ============================================================================
|
|
|
|
func TestEncryptPlaintextSessions(t *testing.T) {
|
|
store, db := setupEncryptionTestStore(t)
|
|
ctx := context.Background()
|
|
now := time.Now().UTC().Format("2006-01-02 15:04:05")
|
|
future := time.Now().Add(time.Hour).UTC().Format("2006-01-02 15:04:05")
|
|
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO sessions (token, encryption_status, expires_at, created_at, updated_at)
|
|
VALUES (?, 'plain_text', ?, ?, ?)`,
|
|
"session-token-1", future, now, now)
|
|
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO sessions (token, encryption_status, expires_at, created_at, updated_at)
|
|
VALUES (?, 'plain_text', ?, ?, ?)`,
|
|
"session-token-2", future, now, now)
|
|
|
|
count, err := store.encryptPlaintextSessions(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 2, count)
|
|
|
|
// Both should be decryptable via GORM
|
|
var sessions []tables.SessionsTable
|
|
require.NoError(t, db.Find(&sessions).Error)
|
|
assert.Len(t, sessions, 2)
|
|
|
|
tokens := map[string]bool{}
|
|
for _, s := range sessions {
|
|
tokens[s.Token] = true
|
|
}
|
|
assert.True(t, tokens["session-token-1"])
|
|
assert.True(t, tokens["session-token-2"])
|
|
}
|
|
|
|
func TestEncryptPlaintextOAuthTokens(t *testing.T) {
|
|
store, db := setupEncryptionTestStore(t)
|
|
ctx := context.Background()
|
|
now := time.Now().UTC().Format("2006-01-02 15:04:05")
|
|
future := time.Now().Add(time.Hour).UTC().Format("2006-01-02 15:04:05")
|
|
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO oauth_tokens (id, access_token, refresh_token, token_type, encryption_status, expires_at, created_at, updated_at)
|
|
VALUES (?, ?, ?, 'Bearer', 'plain_text', ?, ?, ?)`,
|
|
"tok-batch-1", "access-1", "refresh-1", future, now, now)
|
|
|
|
count, err := store.encryptPlaintextOAuthTokens(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, count)
|
|
|
|
var found tables.TableOauthToken
|
|
require.NoError(t, db.First(&found, "id = ?", "tok-batch-1").Error)
|
|
assert.Equal(t, "access-1", found.AccessToken)
|
|
assert.Equal(t, "refresh-1", found.RefreshToken)
|
|
}
|
|
|
|
func TestEncryptPlaintextPlugins(t *testing.T) {
|
|
store, db := setupEncryptionTestStore(t)
|
|
ctx := context.Background()
|
|
now := time.Now().UTC().Format("2006-01-02 15:04:05")
|
|
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO config_plugins (name, enabled, version, config_json, encryption_status, created_at, updated_at)
|
|
VALUES (?, ?, 1, ?, 'plain_text', ?, ?)`,
|
|
"batch-plugin", true, `{"secret":"value"}`, now, now)
|
|
|
|
count, err := store.encryptPlaintextPlugins(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, count)
|
|
|
|
var raw map[string]any
|
|
db.Table("config_plugins").Where("name = ?", "batch-plugin").Take(&raw)
|
|
assert.Equal(t, "encrypted", raw["encryption_status"])
|
|
assert.NotContains(t, raw["config_json"], "secret")
|
|
}
|
|
|
|
func TestEncryptPlaintextPlugins_SkipsEmptyConfig(t *testing.T) {
|
|
store, db := setupEncryptionTestStore(t)
|
|
ctx := context.Background()
|
|
now := time.Now().UTC().Format("2006-01-02 15:04:05")
|
|
|
|
// Insert plugin with empty config — should NOT be picked up by the query
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO config_plugins (name, enabled, version, config_json, encryption_status, created_at, updated_at)
|
|
VALUES (?, ?, 1, '{}', 'plain_text', ?, ?)`,
|
|
"empty-config-plugin", true, now, now)
|
|
|
|
count, err := store.encryptPlaintextPlugins(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 0, count)
|
|
}
|
|
|
|
func TestEncryptPlaintextProviderProxies_SkipsNoProxy(t *testing.T) {
|
|
store, db := setupEncryptionTestStore(t)
|
|
ctx := context.Background()
|
|
now := time.Now().UTC().Format("2006-01-02 15:04:05")
|
|
|
|
// Provider without proxy config — should NOT be picked up
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO config_providers (name, proxy_config_json, encryption_status, created_at, updated_at)
|
|
VALUES (?, '', 'plain_text', ?, ?)`,
|
|
"no-proxy-provider", now, now)
|
|
|
|
count, err := store.encryptPlaintextProviderProxies(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 0, count)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Direct tests for each startup batch function with data verification
|
|
// ============================================================================
|
|
|
|
func TestEncryptPlaintextKeys_EncryptsAndDecryptsCorrectly(t *testing.T) {
|
|
store, db := setupEncryptionTestStore(t)
|
|
ctx := context.Background()
|
|
now := time.Now().UTC().Format("2006-01-02 15:04:05")
|
|
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO config_keys (name, provider_id, provider, key_id, value, encryption_status, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, 'plain_text', ?, ?)`,
|
|
"batch-key-1", 1, "openai", "bk-1", "sk-batch-secret-1", now, now)
|
|
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO config_keys (name, provider_id, provider, key_id, value, encryption_status, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, 'plain_text', ?, ?)`,
|
|
"batch-key-2", 1, "anthropic", "bk-2", "sk-batch-secret-2", now, now)
|
|
|
|
count, err := store.encryptPlaintextKeys(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 2, count)
|
|
|
|
// Raw DB should have encrypted values
|
|
var raw1 map[string]any
|
|
db.Table("config_keys").Where("name = ?", "batch-key-1").Take(&raw1)
|
|
assert.Equal(t, "encrypted", raw1["encryption_status"])
|
|
assert.NotEqual(t, "sk-batch-secret-1", raw1["value"])
|
|
|
|
// GORM hooks should decrypt on read
|
|
var found1 tables.TableKey
|
|
require.NoError(t, db.Where("name = ?", "batch-key-1").First(&found1).Error)
|
|
assert.Equal(t, "sk-batch-secret-1", found1.Value.GetValue())
|
|
|
|
var found2 tables.TableKey
|
|
require.NoError(t, db.Where("name = ?", "batch-key-2").First(&found2).Error)
|
|
assert.Equal(t, "sk-batch-secret-2", found2.Value.GetValue())
|
|
}
|
|
|
|
func TestEncryptPlaintextVirtualKeys_EncryptsAndDecryptsCorrectly(t *testing.T) {
|
|
store, db := setupEncryptionTestStore(t)
|
|
ctx := context.Background()
|
|
now := time.Now().UTC().Format("2006-01-02 15:04:05")
|
|
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO governance_virtual_keys (id, name, value, is_active, encryption_status, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, 'plain_text', ?, ?)`,
|
|
"vk-batch-1", "batch-vk", "vk-batch-secret", true, now, now)
|
|
|
|
count, err := store.encryptPlaintextVirtualKeys(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, count)
|
|
|
|
// Raw DB should have encrypted value
|
|
var raw map[string]any
|
|
db.Table("governance_virtual_keys").Where("id = ?", "vk-batch-1").Take(&raw)
|
|
assert.Equal(t, "encrypted", raw["encryption_status"])
|
|
assert.NotEqual(t, "vk-batch-secret", raw["value"])
|
|
|
|
// GORM hooks should decrypt on read
|
|
var found tables.TableVirtualKey
|
|
require.NoError(t, db.Where("id = ?", "vk-batch-1").First(&found).Error)
|
|
assert.Equal(t, "vk-batch-secret", found.Value)
|
|
}
|
|
|
|
func TestEncryptPlaintextOAuthConfigs_EncryptsAndDecryptsCorrectly(t *testing.T) {
|
|
store, db := setupEncryptionTestStore(t)
|
|
ctx := context.Background()
|
|
now := time.Now().UTC().Format("2006-01-02 15:04:05")
|
|
future := time.Now().Add(time.Hour).UTC().Format("2006-01-02 15:04:05")
|
|
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO oauth_configs (id, client_secret, code_verifier, redirect_uri, state, status, encryption_status, created_at, updated_at, expires_at)
|
|
VALUES (?, ?, ?, ?, ?, 'pending', 'plain_text', ?, ?, ?)`,
|
|
"cfg-batch-1", "batch-client-secret", "batch-verifier", "https://example.com/cb", "csrf", now, now, future)
|
|
|
|
count, err := store.encryptPlaintextOAuthConfigs(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, count)
|
|
|
|
// Raw DB should have encrypted values
|
|
var raw map[string]any
|
|
db.Table("oauth_configs").Where("id = ?", "cfg-batch-1").Take(&raw)
|
|
assert.Equal(t, "encrypted", raw["encryption_status"])
|
|
assert.NotEqual(t, "batch-client-secret", raw["client_secret"])
|
|
assert.NotEqual(t, "batch-verifier", raw["code_verifier"])
|
|
|
|
// GORM hooks should decrypt on read
|
|
var found tables.TableOauthConfig
|
|
require.NoError(t, db.Where("id = ?", "cfg-batch-1").First(&found).Error)
|
|
assert.Equal(t, "batch-client-secret", found.ClientSecret)
|
|
assert.Equal(t, "batch-verifier", found.CodeVerifier)
|
|
}
|
|
|
|
func TestEncryptPlaintextMCPClients_EncryptsAndDecryptsCorrectly(t *testing.T) {
|
|
store, db := setupEncryptionTestStore(t)
|
|
ctx := context.Background()
|
|
now := time.Now().UTC().Format("2006-01-02 15:04:05")
|
|
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO config_mcp_clients (client_id, name, connection_type, connection_string, headers_json, encryption_status, created_at, updated_at)
|
|
VALUES (?, ?, 'sse', ?, ?, 'plain_text', ?, ?)`,
|
|
"mcp-batch-1", "batch-mcp", "https://mcp.example.com", `{"X-Api-Key":"secret-key"}`, now, now)
|
|
|
|
count, err := store.encryptPlaintextMCPClients(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, count)
|
|
|
|
// Raw DB should have encrypted values
|
|
var raw map[string]any
|
|
db.Table("config_mcp_clients").Where("client_id = ?", "mcp-batch-1").Take(&raw)
|
|
assert.Equal(t, "encrypted", raw["encryption_status"])
|
|
assert.NotContains(t, raw["headers_json"], "secret-key")
|
|
|
|
// GORM hooks should decrypt on read
|
|
var found tables.TableMCPClient
|
|
require.NoError(t, db.Where("client_id = ?", "mcp-batch-1").First(&found).Error)
|
|
assert.Equal(t, "https://mcp.example.com", found.ConnectionString.GetValue())
|
|
assert.Equal(t, "secret-key", found.Headers["X-Api-Key"].Val)
|
|
}
|
|
|
|
func TestEncryptPlaintextProviderProxies_EncryptsAndDecryptsCorrectly(t *testing.T) {
|
|
store, db := setupEncryptionTestStore(t)
|
|
ctx := context.Background()
|
|
now := time.Now().UTC().Format("2006-01-02 15:04:05")
|
|
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO config_providers (name, proxy_config_json, encryption_status, created_at, updated_at)
|
|
VALUES (?, ?, 'plain_text', ?, ?)`,
|
|
"proxy-provider", `{"url":"https://proxy.example.com","username":"admin","password":"secret-proxy-pass"}`, now, now)
|
|
|
|
count, err := store.encryptPlaintextProviderProxies(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, count)
|
|
|
|
// Raw DB should have encrypted proxy config
|
|
var raw map[string]any
|
|
db.Table("config_providers").Where("name = ?", "proxy-provider").Take(&raw)
|
|
assert.Equal(t, "encrypted", raw["encryption_status"])
|
|
assert.NotContains(t, raw["proxy_config_json"], "secret-proxy-pass")
|
|
|
|
// GORM hooks should decrypt and deserialize on read
|
|
var found tables.TableProvider
|
|
require.NoError(t, db.Where("name = ?", "proxy-provider").First(&found).Error)
|
|
require.NotNil(t, found.ProxyConfig)
|
|
assert.Equal(t, "https://proxy.example.com", found.ProxyConfig.URL)
|
|
assert.Equal(t, "secret-proxy-pass", found.ProxyConfig.Password)
|
|
}
|
|
|
|
func TestEncryptPlaintextVectorStoreConfigs_EncryptsAndDecryptsCorrectly(t *testing.T) {
|
|
store, db := setupEncryptionTestStore(t)
|
|
ctx := context.Background()
|
|
now := time.Now().UTC().Format("2006-01-02 15:04:05")
|
|
|
|
configJSON := `{"host":"redis.example.com","password":"redis-secret"}`
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO config_vector_store (enabled, type, config, encryption_status, created_at, updated_at)
|
|
VALUES (?, ?, ?, 'plain_text', ?, ?)`,
|
|
true, "redis", configJSON, now, now)
|
|
|
|
count, err := store.encryptPlaintextVectorStoreConfigs(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, count)
|
|
|
|
// Raw DB should have encrypted config
|
|
var raw map[string]any
|
|
db.Table("config_vector_store").Take(&raw)
|
|
assert.Equal(t, "encrypted", raw["encryption_status"])
|
|
assert.NotContains(t, raw["config"], "redis-secret")
|
|
|
|
// GORM hooks should decrypt on read
|
|
var found tables.TableVectorStoreConfig
|
|
require.NoError(t, db.First(&found).Error)
|
|
require.NotNil(t, found.Config)
|
|
assert.Contains(t, *found.Config, "redis-secret")
|
|
}
|
|
|
|
func TestEncryptPlaintextVectorStoreConfigs_SkipsEmptyConfig(t *testing.T) {
|
|
store, db := setupEncryptionTestStore(t)
|
|
ctx := context.Background()
|
|
now := time.Now().UTC().Format("2006-01-02 15:04:05")
|
|
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO config_vector_store (enabled, type, config, encryption_status, created_at, updated_at)
|
|
VALUES (?, ?, '', 'plain_text', ?, ?)`,
|
|
false, "none", now, now)
|
|
|
|
count, err := store.encryptPlaintextVectorStoreConfigs(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 0, count)
|
|
}
|
|
|
|
func TestEncryptPlaintextMCPClients_SkipsEmptyFields(t *testing.T) {
|
|
store, db := setupEncryptionTestStore(t)
|
|
ctx := context.Background()
|
|
now := time.Now().UTC().Format("2006-01-02 15:04:05")
|
|
|
|
// MCP client with no connection string and empty headers — nothing to encrypt
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO config_mcp_clients (client_id, name, connection_type, headers_json, encryption_status, created_at, updated_at)
|
|
VALUES (?, ?, 'stdio', '{}', 'plain_text', ?, ?)`,
|
|
"mcp-empty", "empty-mcp", now, now)
|
|
|
|
count, err := store.encryptPlaintextMCPClients(ctx)
|
|
require.NoError(t, err)
|
|
// Row is still processed (encryption_status changes) even if no fields are encrypted
|
|
assert.Equal(t, 1, count)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Batch pagination — verify >100 rows are handled correctly
|
|
// ============================================================================
|
|
|
|
func TestEncryptPlaintextKeys_MultipleBatches(t *testing.T) {
|
|
store, db := setupEncryptionTestStore(t)
|
|
ctx := context.Background()
|
|
now := time.Now().UTC().Format("2006-01-02 15:04:05")
|
|
|
|
// Insert 5 plaintext keys to verify the batch loop processes all rows
|
|
for i := range 5 {
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO config_keys (name, provider_id, provider, key_id, value, encryption_status, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, 'plain_text', ?, ?)`,
|
|
fmt.Sprintf("paginated-key-%d", i), 1, "openai", fmt.Sprintf("pk-%d", i), fmt.Sprintf("sk-secret-%d", i), now, now)
|
|
}
|
|
|
|
count, err := store.encryptPlaintextKeys(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 5, count)
|
|
|
|
// Verify all are encrypted in raw DB
|
|
var encryptedCount int64
|
|
db.Table("config_keys").Where("encryption_status = ?", "encrypted").Count(&encryptedCount)
|
|
assert.Equal(t, int64(5), encryptedCount)
|
|
|
|
// Verify no plaintext rows remain
|
|
var plaintextCount int64
|
|
db.Table("config_keys").Where("encryption_status = ? OR encryption_status IS NULL OR encryption_status = ''", "plain_text").Count(&plaintextCount)
|
|
assert.Equal(t, int64(0), plaintextCount)
|
|
|
|
// Verify each row is still readable via GORM hooks
|
|
for i := range 5 {
|
|
var found tables.TableKey
|
|
require.NoError(t, db.Where("name = ?", fmt.Sprintf("paginated-key-%d", i)).First(&found).Error)
|
|
assert.Equal(t, fmt.Sprintf("sk-secret-%d", i), found.Value.GetValue())
|
|
}
|
|
}
|
|
|
|
func TestEncryptPlaintextSessions_MultipleBatches(t *testing.T) {
|
|
store, db := setupEncryptionTestStore(t)
|
|
ctx := context.Background()
|
|
now := time.Now().UTC().Format("2006-01-02 15:04:05")
|
|
future := time.Now().Add(time.Hour).UTC().Format("2006-01-02 15:04:05")
|
|
|
|
// Insert 5 plaintext sessions
|
|
for i := range 5 {
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO sessions (token, encryption_status, expires_at, created_at, updated_at)
|
|
VALUES (?, 'plain_text', ?, ?, ?)`,
|
|
fmt.Sprintf("session-token-%d", i), future, now, now)
|
|
}
|
|
|
|
count, err := store.encryptPlaintextSessions(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 5, count)
|
|
|
|
// Verify all are encrypted
|
|
var encryptedCount int64
|
|
db.Table("sessions").Where("encryption_status = ?", "encrypted").Count(&encryptedCount)
|
|
assert.Equal(t, int64(5), encryptedCount)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Provider-specific encrypted fields on TableKey (Azure, Vertex, Bedrock)
|
|
// ============================================================================
|
|
|
|
func TestEncryptPlaintextKeys_AzureFields_EncryptsAndDecryptsCorrectly(t *testing.T) {
|
|
store, db := setupEncryptionTestStore(t)
|
|
ctx := context.Background()
|
|
now := time.Now().UTC().Format("2006-01-02 15:04:05")
|
|
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO config_keys (name, provider_id, provider, key_id, value, azure_endpoint, azure_client_id, azure_client_secret, azure_tenant_id, azure_api_version, encryption_status, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'plain_text', ?, ?)`,
|
|
"azure-key", 1, "azure", "az-1", "sk-azure-key-value",
|
|
"https://myresource.openai.azure.com", "my-azure-client-id", "azure-super-secret-client",
|
|
"my-azure-tenant-id", "2024-10-21", now, now)
|
|
|
|
count, err := store.encryptPlaintextKeys(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, count)
|
|
|
|
// Raw DB should have encrypted values for all sensitive fields
|
|
var raw map[string]any
|
|
db.Table("config_keys").Where("name = ?", "azure-key").Take(&raw)
|
|
assert.Equal(t, "encrypted", raw["encryption_status"])
|
|
assert.NotEqual(t, "sk-azure-key-value", raw["value"])
|
|
assert.NotEqual(t, "https://myresource.openai.azure.com", raw["azure_endpoint"])
|
|
assert.NotEqual(t, "my-azure-client-id", raw["azure_client_id"])
|
|
assert.NotEqual(t, "azure-super-secret-client", raw["azure_client_secret"])
|
|
assert.NotEqual(t, "my-azure-tenant-id", raw["azure_tenant_id"])
|
|
assert.NotEqual(t, "2024-10-21", raw["azure_api_version"])
|
|
|
|
// GORM hooks should decrypt and reconstruct AzureKeyConfig
|
|
var found tables.TableKey
|
|
require.NoError(t, db.Where("name = ?", "azure-key").First(&found).Error)
|
|
assert.Equal(t, "sk-azure-key-value", found.Value.GetValue())
|
|
require.NotNil(t, found.AzureKeyConfig)
|
|
assert.Equal(t, "https://myresource.openai.azure.com", found.AzureKeyConfig.Endpoint.GetValue())
|
|
require.NotNil(t, found.AzureKeyConfig.ClientID)
|
|
assert.Equal(t, "my-azure-client-id", found.AzureKeyConfig.ClientID.GetValue())
|
|
assert.NotNil(t, found.AzureKeyConfig.ClientSecret)
|
|
assert.Equal(t, "azure-super-secret-client", found.AzureKeyConfig.ClientSecret.GetValue())
|
|
require.NotNil(t, found.AzureKeyConfig.TenantID)
|
|
assert.Equal(t, "my-azure-tenant-id", found.AzureKeyConfig.TenantID.GetValue())
|
|
require.NotNil(t, found.AzureKeyConfig.APIVersion)
|
|
assert.Equal(t, "2024-10-21", found.AzureKeyConfig.APIVersion.GetValue())
|
|
}
|
|
|
|
func TestEncryptPlaintextKeys_VertexFields_EncryptsAndDecryptsCorrectly(t *testing.T) {
|
|
store, db := setupEncryptionTestStore(t)
|
|
ctx := context.Background()
|
|
now := time.Now().UTC().Format("2006-01-02 15:04:05")
|
|
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO config_keys (name, provider_id, provider, key_id, value, vertex_project_id, vertex_project_number, vertex_region, vertex_auth_credentials, encryption_status, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'plain_text', ?, ?)`,
|
|
"vertex-key", 1, "vertex", "vx-1", "sk-vertex-key-value",
|
|
"my-gcp-project", "123456789", "us-central1",
|
|
`{"type":"service_account","private_key":"-----BEGIN PRIVATE KEY-----secret"}`, now, now)
|
|
|
|
count, err := store.encryptPlaintextKeys(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, count)
|
|
|
|
// Raw DB should have encrypted values
|
|
var raw map[string]any
|
|
db.Table("config_keys").Where("name = ?", "vertex-key").Take(&raw)
|
|
assert.Equal(t, "encrypted", raw["encryption_status"])
|
|
assert.NotEqual(t, "sk-vertex-key-value", raw["value"])
|
|
assert.NotEqual(t, "my-gcp-project", raw["vertex_project_id"])
|
|
assert.NotEqual(t, "123456789", raw["vertex_project_number"])
|
|
assert.NotEqual(t, "us-central1", raw["vertex_region"])
|
|
assert.NotContains(t, fmt.Sprintf("%v", raw["vertex_auth_credentials"]), "private_key")
|
|
|
|
// GORM hooks should decrypt and reconstruct VertexKeyConfig
|
|
var found tables.TableKey
|
|
require.NoError(t, db.Where("name = ?", "vertex-key").First(&found).Error)
|
|
assert.Equal(t, "sk-vertex-key-value", found.Value.GetValue())
|
|
require.NotNil(t, found.VertexKeyConfig)
|
|
assert.Equal(t, "my-gcp-project", found.VertexKeyConfig.ProjectID.GetValue())
|
|
assert.Equal(t, "123456789", found.VertexKeyConfig.ProjectNumber.GetValue())
|
|
assert.Equal(t, "us-central1", found.VertexKeyConfig.Region.GetValue())
|
|
assert.Contains(t, found.VertexKeyConfig.AuthCredentials.GetValue(), "private_key")
|
|
}
|
|
|
|
func TestEncryptPlaintextKeys_BedrockFields_EncryptsAndDecryptsCorrectly(t *testing.T) {
|
|
store, db := setupEncryptionTestStore(t)
|
|
ctx := context.Background()
|
|
now := time.Now().UTC().Format("2006-01-02 15:04:05")
|
|
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO config_keys (name, provider_id, provider, key_id, value, bedrock_access_key, bedrock_secret_key, bedrock_session_token, bedrock_region, bedrock_arn, aliases_json, bedrock_batch_s3_config_json, encryption_status, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'plain_text', ?, ?)`,
|
|
"bedrock-key", 1, "bedrock", "br-1", "sk-bedrock-key-value",
|
|
"AKIAIOSFODNN7EXAMPLE", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", "FwoGZXIvYXdzEBYaDH7sampleSessionToken",
|
|
"us-west-2", "arn:aws:iam::123456789:role/bedrock",
|
|
`{"claude-3":"profile-claude"}`, `{"buckets":[{"bucket_name":"my-bucket","prefix":"jobs/","is_default":true}]}`,
|
|
now, now)
|
|
|
|
count, err := store.encryptPlaintextKeys(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, count)
|
|
|
|
// Raw DB should have encrypted values for all Bedrock fields
|
|
var raw map[string]any
|
|
db.Table("config_keys").Where("name = ?", "bedrock-key").Take(&raw)
|
|
assert.Equal(t, "encrypted", raw["encryption_status"])
|
|
assert.NotEqual(t, "sk-bedrock-key-value", raw["value"])
|
|
assert.NotEqual(t, "AKIAIOSFODNN7EXAMPLE", raw["bedrock_access_key"])
|
|
assert.NotEqual(t, "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", raw["bedrock_secret_key"])
|
|
assert.NotEqual(t, "FwoGZXIvYXdzEBYaDH7sampleSessionToken", raw["bedrock_session_token"])
|
|
assert.NotEqual(t, "us-west-2", raw["bedrock_region"])
|
|
assert.NotEqual(t, "arn:aws:iam::123456789:role/bedrock", raw["bedrock_arn"])
|
|
rawAliasesVal := raw["aliases_json"]
|
|
var rawAliasesStr string
|
|
switch v := rawAliasesVal.(type) {
|
|
case string:
|
|
rawAliasesStr = v
|
|
case []byte:
|
|
rawAliasesStr = string(v)
|
|
}
|
|
assert.NotContains(t, rawAliasesStr, "profile-claude")
|
|
if rawBatch, ok := raw["bedrock_batch_s3_config_json"].(string); ok {
|
|
assert.NotContains(t, rawBatch, "my-bucket")
|
|
}
|
|
|
|
// GORM hooks should decrypt and reconstruct BedrockKeyConfig
|
|
var found tables.TableKey
|
|
require.NoError(t, db.Where("name = ?", "bedrock-key").First(&found).Error)
|
|
assert.Equal(t, "sk-bedrock-key-value", found.Value.GetValue())
|
|
require.NotNil(t, found.BedrockKeyConfig)
|
|
assert.Equal(t, "AKIAIOSFODNN7EXAMPLE", found.BedrockKeyConfig.AccessKey.GetValue())
|
|
assert.Equal(t, "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", found.BedrockKeyConfig.SecretKey.GetValue())
|
|
require.NotNil(t, found.BedrockKeyConfig.SessionToken)
|
|
assert.Equal(t, "FwoGZXIvYXdzEBYaDH7sampleSessionToken", found.BedrockKeyConfig.SessionToken.GetValue())
|
|
require.NotNil(t, found.BedrockKeyConfig.Region)
|
|
assert.Equal(t, "us-west-2", found.BedrockKeyConfig.Region.GetValue())
|
|
require.NotNil(t, found.BedrockKeyConfig.ARN)
|
|
assert.Equal(t, "arn:aws:iam::123456789:role/bedrock", found.BedrockKeyConfig.ARN.GetValue())
|
|
assert.Equal(t, "profile-claude", found.Aliases["claude-3"])
|
|
require.NotNil(t, found.BedrockKeyConfig.BatchS3Config)
|
|
require.Len(t, found.BedrockKeyConfig.BatchS3Config.Buckets, 1)
|
|
assert.Equal(t, "my-bucket", found.BedrockKeyConfig.BatchS3Config.Buckets[0].BucketName)
|
|
}
|
|
|
|
func TestEncryptPlaintextKeys_AllProviderFields_ViaStartupPass(t *testing.T) {
|
|
store, db := setupEncryptionTestStore(t)
|
|
ctx := context.Background()
|
|
now := time.Now().UTC().Format("2006-01-02 15:04:05")
|
|
|
|
// Insert keys for all three providers with sensitive fields
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO config_keys (name, provider_id, provider, key_id, value, azure_endpoint, azure_client_secret, encryption_status, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, 'plain_text', ?, ?)`,
|
|
"startup-azure", 1, "azure", "sa-1", "sk-az", "https://az.openai.azure.com", "az-secret", now, now)
|
|
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO config_keys (name, provider_id, provider, key_id, value, vertex_auth_credentials, encryption_status, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, 'plain_text', ?, ?)`,
|
|
"startup-vertex", 1, "vertex", "sv-1", "sk-vx", "vertex-creds-json", now, now)
|
|
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO config_keys (name, provider_id, provider, key_id, value, bedrock_access_key, bedrock_secret_key, bedrock_session_token, encryption_status, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'plain_text', ?, ?)`,
|
|
"startup-bedrock", 1, "bedrock", "sb-1", "sk-br", "AKIA-BR", "secret-br", "session-br", now, now)
|
|
|
|
// Run the full startup encryption pass
|
|
err := store.EncryptPlaintextRows(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// Verify all three rows are encrypted in raw DB
|
|
for _, name := range []string{"startup-azure", "startup-vertex", "startup-bedrock"} {
|
|
var raw map[string]any
|
|
db.Table("config_keys").Where("name = ?", name).Take(&raw)
|
|
assert.Equal(t, "encrypted", raw["encryption_status"], "expected encrypted status for %s", name)
|
|
}
|
|
|
|
// Verify Azure fields survived round-trip
|
|
var azKey tables.TableKey
|
|
require.NoError(t, db.Where("name = ?", "startup-azure").First(&azKey).Error)
|
|
assert.Equal(t, "sk-az", azKey.Value.GetValue())
|
|
require.NotNil(t, azKey.AzureKeyConfig)
|
|
assert.Equal(t, "https://az.openai.azure.com", azKey.AzureKeyConfig.Endpoint.GetValue())
|
|
assert.NotNil(t, azKey.AzureKeyConfig.ClientSecret)
|
|
assert.Equal(t, "az-secret", azKey.AzureKeyConfig.ClientSecret.GetValue())
|
|
|
|
// Verify Vertex fields survived round-trip
|
|
var vxKey tables.TableKey
|
|
require.NoError(t, db.Where("name = ?", "startup-vertex").First(&vxKey).Error)
|
|
assert.Equal(t, "sk-vx", vxKey.Value.GetValue())
|
|
require.NotNil(t, vxKey.VertexKeyConfig)
|
|
assert.Equal(t, "vertex-creds-json", vxKey.VertexKeyConfig.AuthCredentials.GetValue())
|
|
|
|
// Verify Bedrock fields survived round-trip
|
|
var brKey tables.TableKey
|
|
require.NoError(t, db.Where("name = ?", "startup-bedrock").First(&brKey).Error)
|
|
assert.Equal(t, "sk-br", brKey.Value.GetValue())
|
|
require.NotNil(t, brKey.BedrockKeyConfig)
|
|
assert.Equal(t, "AKIA-BR", brKey.BedrockKeyConfig.AccessKey.GetValue())
|
|
assert.Equal(t, "secret-br", brKey.BedrockKeyConfig.SecretKey.GetValue())
|
|
require.NotNil(t, brKey.BedrockKeyConfig.SessionToken)
|
|
assert.Equal(t, "session-br", brKey.BedrockKeyConfig.SessionToken.GetValue())
|
|
}
|
|
|
|
// ============================================================================
|
|
// BeforeSave must not mutate shared provider config structs (regression test)
|
|
// ============================================================================
|
|
|
|
func TestBeforeSave_DoesNotMutateSharedProviderConfigs(t *testing.T) {
|
|
_, db := setupEncryptionTestStore(t)
|
|
|
|
// Simulate the startup flow: create a key with AzureKeyConfig set via a shared pointer,
|
|
// save it to DB, and verify the original config structs are not mutated by BeforeSave
|
|
// (encryption uses value-copies so shared pointers are never corrupted).
|
|
azureCfg := &schemas.AzureKeyConfig{
|
|
Endpoint: *schemas.NewEnvVar("https://myresource.openai.azure.com"),
|
|
APIVersion: schemas.NewEnvVar("2024-10-21"),
|
|
ClientID: schemas.NewEnvVar("my-azure-client-id"),
|
|
TenantID: schemas.NewEnvVar("my-azure-tenant-id"),
|
|
}
|
|
azureCfg.ClientSecret = schemas.NewEnvVar("azure-client-secret")
|
|
|
|
vertexCfg := &schemas.VertexKeyConfig{
|
|
ProjectID: *schemas.NewEnvVar("my-project"),
|
|
ProjectNumber: *schemas.NewEnvVar("123456789"),
|
|
Region: *schemas.NewEnvVar("us-central1"),
|
|
AuthCredentials: *schemas.NewEnvVar("vertex-creds"),
|
|
}
|
|
|
|
bedrockCfg := &schemas.BedrockKeyConfig{
|
|
AccessKey: *schemas.NewEnvVar("AKIAEXAMPLE"),
|
|
SecretKey: *schemas.NewEnvVar("secret-key"),
|
|
SessionToken: schemas.NewEnvVar("session-tok"),
|
|
Region: schemas.NewEnvVar("us-east-1"),
|
|
ARN: schemas.NewEnvVar("arn:aws:iam::123456789:role/test"),
|
|
}
|
|
|
|
// Save a key using the shared config pointers (mimics UpdateProvidersConfig)
|
|
key := &tables.TableKey{
|
|
Name: "shared-ptr-test",
|
|
ProviderID: 1,
|
|
Provider: "azure",
|
|
KeyID: "sp-1",
|
|
Value: *schemas.NewEnvVar("sk-test-value"),
|
|
AzureKeyConfig: azureCfg,
|
|
VertexKeyConfig: vertexCfg,
|
|
BedrockKeyConfig: bedrockCfg,
|
|
}
|
|
require.NoError(t, db.Create(key).Error)
|
|
|
|
// The original config structs must NOT have been mutated by BeforeSave.
|
|
// All fields are now encrypted; the value-copy pattern in BeforeSave ensures
|
|
// the caller's shared config struct is never corrupted by in-place encryption.
|
|
|
|
// Azure: encrypted fields
|
|
assert.Equal(t, "https://myresource.openai.azure.com", azureCfg.Endpoint.GetValue(),
|
|
"BeforeSave must not mutate shared AzureKeyConfig.Endpoint")
|
|
assert.Equal(t, "azure-client-secret", azureCfg.ClientSecret.GetValue(),
|
|
"BeforeSave must not mutate shared AzureKeyConfig.ClientSecret")
|
|
assert.Equal(t, "2024-10-21", azureCfg.APIVersion.GetValue(),
|
|
"BeforeSave must not mutate shared AzureKeyConfig.APIVersion")
|
|
assert.Equal(t, "my-azure-client-id", azureCfg.ClientID.GetValue(),
|
|
"BeforeSave must not mutate shared AzureKeyConfig.ClientID")
|
|
assert.Equal(t, "my-azure-tenant-id", azureCfg.TenantID.GetValue(),
|
|
"BeforeSave must not mutate shared AzureKeyConfig.TenantID")
|
|
|
|
// Vertex: encrypted fields
|
|
assert.Equal(t, "vertex-creds", vertexCfg.AuthCredentials.GetValue(),
|
|
"BeforeSave must not mutate shared VertexKeyConfig.AuthCredentials")
|
|
assert.Equal(t, "my-project", vertexCfg.ProjectID.GetValue(),
|
|
"BeforeSave must not mutate shared VertexKeyConfig.ProjectID")
|
|
assert.Equal(t, "123456789", vertexCfg.ProjectNumber.GetValue(),
|
|
"BeforeSave must not mutate shared VertexKeyConfig.ProjectNumber")
|
|
assert.Equal(t, "us-central1", vertexCfg.Region.GetValue(),
|
|
"BeforeSave must not mutate shared VertexKeyConfig.Region")
|
|
|
|
// Bedrock: encrypted fields
|
|
assert.Equal(t, "AKIAEXAMPLE", bedrockCfg.AccessKey.GetValue(),
|
|
"BeforeSave must not mutate shared BedrockKeyConfig.AccessKey")
|
|
assert.Equal(t, "secret-key", bedrockCfg.SecretKey.GetValue(),
|
|
"BeforeSave must not mutate shared BedrockKeyConfig.SecretKey")
|
|
assert.Equal(t, "session-tok", bedrockCfg.SessionToken.GetValue(),
|
|
"BeforeSave must not mutate shared BedrockKeyConfig.SessionToken")
|
|
assert.Equal(t, "us-east-1", bedrockCfg.Region.GetValue(),
|
|
"BeforeSave must not mutate shared BedrockKeyConfig.Region")
|
|
assert.Equal(t, "arn:aws:iam::123456789:role/test", bedrockCfg.ARN.GetValue(),
|
|
"BeforeSave must not mutate shared BedrockKeyConfig.ARN")
|
|
|
|
// Verify the DB round-trip still works (encrypted + decryptable)
|
|
var found tables.TableKey
|
|
require.NoError(t, db.Where("name = ?", "shared-ptr-test").First(&found).Error)
|
|
assert.Equal(t, "sk-test-value", found.Value.GetValue())
|
|
require.NotNil(t, found.AzureKeyConfig)
|
|
assert.Equal(t, "https://myresource.openai.azure.com", found.AzureKeyConfig.Endpoint.GetValue())
|
|
assert.Equal(t, "azure-client-secret", found.AzureKeyConfig.ClientSecret.GetValue())
|
|
assert.Equal(t, "2024-10-21", found.AzureKeyConfig.APIVersion.GetValue())
|
|
assert.Equal(t, "my-azure-client-id", found.AzureKeyConfig.ClientID.GetValue())
|
|
assert.Equal(t, "my-azure-tenant-id", found.AzureKeyConfig.TenantID.GetValue())
|
|
require.NotNil(t, found.VertexKeyConfig)
|
|
assert.Equal(t, "vertex-creds", found.VertexKeyConfig.AuthCredentials.GetValue())
|
|
assert.Equal(t, "my-project", found.VertexKeyConfig.ProjectID.GetValue())
|
|
assert.Equal(t, "123456789", found.VertexKeyConfig.ProjectNumber.GetValue())
|
|
assert.Equal(t, "us-central1", found.VertexKeyConfig.Region.GetValue())
|
|
require.NotNil(t, found.BedrockKeyConfig)
|
|
assert.Equal(t, "AKIAEXAMPLE", found.BedrockKeyConfig.AccessKey.GetValue())
|
|
assert.Equal(t, "secret-key", found.BedrockKeyConfig.SecretKey.GetValue())
|
|
assert.Equal(t, "session-tok", found.BedrockKeyConfig.SessionToken.GetValue())
|
|
assert.Equal(t, "us-east-1", found.BedrockKeyConfig.Region.GetValue())
|
|
assert.Equal(t, "arn:aws:iam::123456789:role/test", found.BedrockKeyConfig.ARN.GetValue())
|
|
}
|
|
|
|
// ============================================================================
|
|
// EnvVar-backed fields must not be encrypted (encryption is a no-op for FromEnv)
|
|
// ============================================================================
|
|
|
|
func TestBeforeSave_EnvVarBackedFields_NotEncrypted(t *testing.T) {
|
|
_, db := setupEncryptionTestStore(t)
|
|
|
|
// Set environment variables that the EnvVars will resolve to
|
|
t.Setenv("TEST_AZURE_KEY", "sk-azure-from-env")
|
|
t.Setenv("TEST_AZURE_ENDPOINT", "https://env-resource.openai.azure.com")
|
|
t.Setenv("TEST_AZURE_SECRET", "env-azure-client-secret")
|
|
t.Setenv("TEST_AZURE_API_VER", "2024-10-21")
|
|
t.Setenv("TEST_AZURE_CLIENT_ID", "env-azure-client-id")
|
|
t.Setenv("TEST_AZURE_TENANT_ID", "env-azure-tenant-id")
|
|
t.Setenv("TEST_VERTEX_PROJECT", "env-vertex-project")
|
|
t.Setenv("TEST_VERTEX_REGION", "env-us-central1")
|
|
t.Setenv("TEST_VERTEX_CREDS", "env-vertex-creds-json")
|
|
t.Setenv("TEST_BEDROCK_ACCESS", "env-AKIA-ACCESS")
|
|
t.Setenv("TEST_BEDROCK_SECRET", "env-bedrock-secret")
|
|
t.Setenv("TEST_BEDROCK_SESSION", "env-bedrock-session")
|
|
t.Setenv("TEST_BEDROCK_REGION", "env-us-east-1")
|
|
t.Setenv("TEST_BEDROCK_ARN", "arn:aws:iam::env:role/test")
|
|
|
|
// Create EnvVars backed by environment variables
|
|
azureCfg := &schemas.AzureKeyConfig{
|
|
Endpoint: *schemas.NewEnvVar("env.TEST_AZURE_ENDPOINT"),
|
|
APIVersion: schemas.NewEnvVar("env.TEST_AZURE_API_VER"),
|
|
ClientID: schemas.NewEnvVar("env.TEST_AZURE_CLIENT_ID"),
|
|
ClientSecret: schemas.NewEnvVar("env.TEST_AZURE_SECRET"),
|
|
TenantID: schemas.NewEnvVar("env.TEST_AZURE_TENANT_ID"),
|
|
}
|
|
vertexCfg := &schemas.VertexKeyConfig{
|
|
ProjectID: *schemas.NewEnvVar("env.TEST_VERTEX_PROJECT"),
|
|
Region: *schemas.NewEnvVar("env.TEST_VERTEX_REGION"),
|
|
AuthCredentials: *schemas.NewEnvVar("env.TEST_VERTEX_CREDS"),
|
|
}
|
|
bedrockCfg := &schemas.BedrockKeyConfig{
|
|
AccessKey: *schemas.NewEnvVar("env.TEST_BEDROCK_ACCESS"),
|
|
SecretKey: *schemas.NewEnvVar("env.TEST_BEDROCK_SECRET"),
|
|
SessionToken: schemas.NewEnvVar("env.TEST_BEDROCK_SESSION"),
|
|
Region: schemas.NewEnvVar("env.TEST_BEDROCK_REGION"),
|
|
ARN: schemas.NewEnvVar("env.TEST_BEDROCK_ARN"),
|
|
}
|
|
|
|
// Verify the EnvVars resolved correctly and are marked as FromEnv
|
|
require.True(t, azureCfg.Endpoint.IsFromEnv())
|
|
require.Equal(t, "https://env-resource.openai.azure.com", azureCfg.Endpoint.GetValue())
|
|
require.True(t, azureCfg.ClientSecret.IsFromEnv())
|
|
require.True(t, vertexCfg.AuthCredentials.IsFromEnv())
|
|
require.True(t, bedrockCfg.AccessKey.IsFromEnv())
|
|
|
|
key := &tables.TableKey{
|
|
Name: "env-backed-key",
|
|
ProviderID: 1,
|
|
Provider: "azure",
|
|
KeyID: "env-1",
|
|
Value: *schemas.NewEnvVar("env.TEST_AZURE_KEY"),
|
|
AzureKeyConfig: azureCfg,
|
|
VertexKeyConfig: vertexCfg,
|
|
BedrockKeyConfig: bedrockCfg,
|
|
}
|
|
require.NoError(t, db.Create(key).Error)
|
|
|
|
// Raw DB should store the env var references, NOT encrypted ciphertext.
|
|
// EnvVar.Value() returns the env var name (e.g. "env.TEST_AZURE_KEY") when FromEnv=true.
|
|
var raw map[string]any
|
|
db.Table("config_keys").Where("name = ?", "env-backed-key").Take(&raw)
|
|
assert.Equal(t, "encrypted", raw["encryption_status"])
|
|
// Value column should contain the env reference string, not encrypted data
|
|
assert.Equal(t, "env.TEST_AZURE_KEY", raw["value"])
|
|
assert.Equal(t, "env.TEST_AZURE_ENDPOINT", raw["azure_endpoint"])
|
|
assert.Equal(t, "env.TEST_AZURE_SECRET", raw["azure_client_secret"])
|
|
|
|
// The shared config structs must NOT be mutated
|
|
assert.Equal(t, "https://env-resource.openai.azure.com", azureCfg.Endpoint.GetValue())
|
|
assert.True(t, azureCfg.Endpoint.IsFromEnv())
|
|
assert.Equal(t, "env-azure-client-secret", azureCfg.ClientSecret.GetValue())
|
|
assert.True(t, azureCfg.ClientSecret.IsFromEnv())
|
|
assert.Equal(t, "env-vertex-creds-json", vertexCfg.AuthCredentials.GetValue())
|
|
assert.True(t, vertexCfg.AuthCredentials.IsFromEnv())
|
|
assert.Equal(t, "env-AKIA-ACCESS", bedrockCfg.AccessKey.GetValue())
|
|
assert.True(t, bedrockCfg.AccessKey.IsFromEnv())
|
|
assert.Equal(t, "env-bedrock-secret", bedrockCfg.SecretKey.GetValue())
|
|
assert.True(t, bedrockCfg.SecretKey.IsFromEnv())
|
|
assert.Equal(t, "env-bedrock-session", bedrockCfg.SessionToken.GetValue())
|
|
assert.True(t, bedrockCfg.SessionToken.IsFromEnv())
|
|
|
|
// GORM round-trip: AfterFind should reconstruct env-backed EnvVars correctly
|
|
var found tables.TableKey
|
|
require.NoError(t, db.Where("name = ?", "env-backed-key").First(&found).Error)
|
|
assert.Equal(t, "sk-azure-from-env", found.Value.GetValue())
|
|
assert.True(t, found.Value.IsFromEnv())
|
|
|
|
require.NotNil(t, found.AzureKeyConfig)
|
|
assert.Equal(t, "https://env-resource.openai.azure.com", found.AzureKeyConfig.Endpoint.GetValue())
|
|
assert.True(t, found.AzureKeyConfig.Endpoint.IsFromEnv())
|
|
assert.Equal(t, "env-azure-client-secret", found.AzureKeyConfig.ClientSecret.GetValue())
|
|
assert.True(t, found.AzureKeyConfig.ClientSecret.IsFromEnv())
|
|
assert.Equal(t, "2024-10-21", found.AzureKeyConfig.APIVersion.GetValue())
|
|
assert.True(t, found.AzureKeyConfig.APIVersion.IsFromEnv())
|
|
assert.Equal(t, "env-azure-client-id", found.AzureKeyConfig.ClientID.GetValue())
|
|
assert.True(t, found.AzureKeyConfig.ClientID.IsFromEnv())
|
|
assert.Equal(t, "env-azure-tenant-id", found.AzureKeyConfig.TenantID.GetValue())
|
|
assert.True(t, found.AzureKeyConfig.TenantID.IsFromEnv())
|
|
|
|
require.NotNil(t, found.VertexKeyConfig)
|
|
assert.Equal(t, "env-vertex-project", found.VertexKeyConfig.ProjectID.GetValue())
|
|
assert.True(t, found.VertexKeyConfig.ProjectID.IsFromEnv())
|
|
assert.Equal(t, "env-us-central1", found.VertexKeyConfig.Region.GetValue())
|
|
assert.True(t, found.VertexKeyConfig.Region.IsFromEnv())
|
|
assert.Equal(t, "env-vertex-creds-json", found.VertexKeyConfig.AuthCredentials.GetValue())
|
|
assert.True(t, found.VertexKeyConfig.AuthCredentials.IsFromEnv())
|
|
|
|
require.NotNil(t, found.BedrockKeyConfig)
|
|
assert.Equal(t, "env-AKIA-ACCESS", found.BedrockKeyConfig.AccessKey.GetValue())
|
|
assert.True(t, found.BedrockKeyConfig.AccessKey.IsFromEnv())
|
|
assert.Equal(t, "env-bedrock-secret", found.BedrockKeyConfig.SecretKey.GetValue())
|
|
assert.True(t, found.BedrockKeyConfig.SecretKey.IsFromEnv())
|
|
assert.Equal(t, "env-bedrock-session", found.BedrockKeyConfig.SessionToken.GetValue())
|
|
assert.True(t, found.BedrockKeyConfig.SessionToken.IsFromEnv())
|
|
assert.Equal(t, "env-us-east-1", found.BedrockKeyConfig.Region.GetValue())
|
|
assert.True(t, found.BedrockKeyConfig.Region.IsFromEnv())
|
|
assert.Equal(t, "arn:aws:iam::env:role/test", found.BedrockKeyConfig.ARN.GetValue())
|
|
assert.True(t, found.BedrockKeyConfig.ARN.IsFromEnv())
|
|
}
|
|
|
|
func TestEncryptPlaintextKeys_EnvVarBackedFields_SurviveStartupPass(t *testing.T) {
|
|
store, db := setupEncryptionTestStore(t)
|
|
ctx := context.Background()
|
|
now := time.Now().UTC().Format("2006-01-02 15:04:05")
|
|
|
|
t.Setenv("TEST_SP_KEY", "sk-startup-env-key")
|
|
t.Setenv("TEST_SP_ENDPOINT", "https://startup.openai.azure.com")
|
|
t.Setenv("TEST_SP_CREDS", "startup-vertex-creds")
|
|
t.Setenv("TEST_SP_ACCESS", "AKIA-STARTUP")
|
|
|
|
// Insert plaintext rows with env var references via raw SQL (mimics legacy data)
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO config_keys (name, provider_id, provider, key_id, value, azure_endpoint, vertex_auth_credentials, bedrock_access_key, encryption_status, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'plain_text', ?, ?)`,
|
|
"env-startup-key", 1, "azure", "esp-1",
|
|
"env.TEST_SP_KEY", "env.TEST_SP_ENDPOINT", "env.TEST_SP_CREDS", "env.TEST_SP_ACCESS",
|
|
now, now)
|
|
|
|
// Run the startup encryption pass
|
|
count, err := store.encryptPlaintextKeys(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, count)
|
|
|
|
// Raw DB should still have env var references (not encrypted ciphertext)
|
|
var raw map[string]any
|
|
db.Table("config_keys").Where("name = ?", "env-startup-key").Take(&raw)
|
|
assert.Equal(t, "encrypted", raw["encryption_status"])
|
|
assert.Equal(t, "env.TEST_SP_KEY", raw["value"])
|
|
assert.Equal(t, "env.TEST_SP_ENDPOINT", raw["azure_endpoint"])
|
|
assert.Equal(t, "env.TEST_SP_CREDS", raw["vertex_auth_credentials"])
|
|
assert.Equal(t, "env.TEST_SP_ACCESS", raw["bedrock_access_key"])
|
|
|
|
// GORM should resolve env vars on read
|
|
var found tables.TableKey
|
|
require.NoError(t, db.Where("name = ?", "env-startup-key").First(&found).Error)
|
|
assert.Equal(t, "sk-startup-env-key", found.Value.GetValue())
|
|
assert.True(t, found.Value.IsFromEnv())
|
|
require.NotNil(t, found.AzureKeyConfig)
|
|
assert.Equal(t, "https://startup.openai.azure.com", found.AzureKeyConfig.Endpoint.GetValue())
|
|
assert.True(t, found.AzureKeyConfig.Endpoint.IsFromEnv())
|
|
require.NotNil(t, found.VertexKeyConfig)
|
|
assert.Equal(t, "startup-vertex-creds", found.VertexKeyConfig.AuthCredentials.GetValue())
|
|
assert.True(t, found.VertexKeyConfig.AuthCredentials.IsFromEnv())
|
|
require.NotNil(t, found.BedrockKeyConfig)
|
|
assert.Equal(t, "AKIA-STARTUP", found.BedrockKeyConfig.AccessKey.GetValue())
|
|
assert.True(t, found.BedrockKeyConfig.AccessKey.IsFromEnv())
|
|
}
|
|
|
|
// ============================================================================
|
|
// Encryption disabled — startup pass is a no-op
|
|
// ============================================================================
|
|
|
|
func TestEncryptPlaintextRows_EncryptionDisabled_Noop(t *testing.T) {
|
|
// Disable encryption for this test
|
|
encrypt.Init("", bifrost.NewDefaultLogger(schemas.LogLevelInfo))
|
|
t.Cleanup(func() {
|
|
encrypt.Init(testEncryptionKey, bifrost.NewDefaultLogger(schemas.LogLevelInfo))
|
|
})
|
|
|
|
store, db := setupEncryptionTestStore(t)
|
|
ctx := context.Background()
|
|
now := time.Now().UTC().Format("2006-01-02 15:04:05")
|
|
future := time.Now().Add(time.Hour).UTC().Format("2006-01-02 15:04:05")
|
|
|
|
// Insert plaintext rows across multiple tables
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO config_keys (name, provider_id, provider, key_id, value, encryption_status, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, 'plain_text', ?, ?)`,
|
|
"disabled-key", 1, "openai", "dk-1", "sk-should-stay-plain", now, now)
|
|
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO sessions (token, encryption_status, expires_at, created_at, updated_at)
|
|
VALUES (?, 'plain_text', ?, ?, ?)`,
|
|
"session-should-stay-plain", future, now, now)
|
|
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO governance_virtual_keys (id, name, value, is_active, encryption_status, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, 'plain_text', ?, ?)`,
|
|
"vk-dis-1", "dis-vk", "vk-should-stay-plain", true, now, now)
|
|
|
|
// Run the startup pass — should return immediately (nil) without modifying rows
|
|
err := store.EncryptPlaintextRows(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// Verify all rows remain as plaintext in the raw DB
|
|
var keyRow map[string]any
|
|
db.Table("config_keys").Where("name = ?", "disabled-key").Take(&keyRow)
|
|
assert.Equal(t, "plain_text", keyRow["encryption_status"])
|
|
assert.Equal(t, "sk-should-stay-plain", keyRow["value"])
|
|
|
|
var sessionRow map[string]any
|
|
db.Table("sessions").Take(&sessionRow)
|
|
assert.Equal(t, "plain_text", sessionRow["encryption_status"])
|
|
assert.Equal(t, "session-should-stay-plain", sessionRow["token"])
|
|
|
|
var vkRow map[string]any
|
|
db.Table("governance_virtual_keys").Where("id = ?", "vk-dis-1").Take(&vkRow)
|
|
assert.Equal(t, "plain_text", vkRow["encryption_status"])
|
|
assert.Equal(t, "vk-should-stay-plain", vkRow["value"])
|
|
}
|
|
|
|
func TestEncryptPlaintextRows_EncryptionDisabled_GORMHooksStorePlaintext(t *testing.T) {
|
|
// Disable encryption for this test
|
|
encrypt.Init("", bifrost.NewDefaultLogger(schemas.LogLevelInfo))
|
|
t.Cleanup(func() {
|
|
encrypt.Init(testEncryptionKey, bifrost.NewDefaultLogger(schemas.LogLevelInfo))
|
|
})
|
|
|
|
_, db := setupEncryptionTestStore(t)
|
|
|
|
// Create rows via GORM (hooks fire, but encryption is disabled)
|
|
key := &tables.TableKey{
|
|
Name: "hook-no-encrypt",
|
|
ProviderID: 1,
|
|
Provider: "openai",
|
|
KeyID: "hne-1",
|
|
Value: *schemas.NewEnvVar("sk-stays-plain-via-hook"),
|
|
}
|
|
require.NoError(t, db.Create(key).Error)
|
|
|
|
// Raw DB should have plaintext
|
|
var raw map[string]any
|
|
db.Table("config_keys").Where("id = ?", key.ID).Take(&raw)
|
|
assert.Equal(t, "plain_text", raw["encryption_status"])
|
|
assert.Equal(t, "sk-stays-plain-via-hook", raw["value"])
|
|
|
|
// GORM read should work fine (AfterFind skips decryption for non-encrypted rows)
|
|
var found tables.TableKey
|
|
require.NoError(t, db.First(&found, key.ID).Error)
|
|
assert.Equal(t, "sk-stays-plain-via-hook", found.Value.GetValue())
|
|
}
|
|
|
|
// ============================================================================
|
|
// Empty database — startup pass is a graceful no-op
|
|
// ============================================================================
|
|
|
|
func TestEncryptPlaintextRows_EmptyDatabase(t *testing.T) {
|
|
store, _ := setupEncryptionTestStore(t)
|
|
ctx := context.Background()
|
|
|
|
err := store.EncryptPlaintextRows(ctx)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// ============================================================================
|
|
// OAuthConfigs skip when both secrets are empty
|
|
// ============================================================================
|
|
|
|
func TestEncryptPlaintextOAuthConfigs_SkipsBothEmptySecrets(t *testing.T) {
|
|
store, db := setupEncryptionTestStore(t)
|
|
ctx := context.Background()
|
|
now := time.Now().UTC().Format("2006-01-02 15:04:05")
|
|
future := time.Now().Add(time.Hour).UTC().Format("2006-01-02 15:04:05")
|
|
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO oauth_configs (id, client_secret, code_verifier, redirect_uri, state, status, encryption_status, created_at, updated_at, expires_at)
|
|
VALUES (?, '', '', ?, ?, 'pending', 'plain_text', ?, ?, ?)`,
|
|
"cfg-empty-secrets", "https://example.com/cb", "csrf-state", now, now, future)
|
|
|
|
count, err := store.encryptPlaintextOAuthConfigs(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 0, count)
|
|
|
|
var raw map[string]any
|
|
db.Table("oauth_configs").Where("id = ?", "cfg-empty-secrets").Take(&raw)
|
|
assert.Equal(t, "plain_text", raw["encryption_status"])
|
|
}
|
|
|
|
// ============================================================================
|
|
// Hash computation during startup pass (sessions + virtual keys)
|
|
// ============================================================================
|
|
|
|
func TestEncryptPlaintextSessions_HashComputedDuringStartup(t *testing.T) {
|
|
store, db := setupEncryptionTestStore(t)
|
|
ctx := context.Background()
|
|
now := time.Now().UTC().Format("2006-01-02 15:04:05")
|
|
future := time.Now().Add(time.Hour).UTC().Format("2006-01-02 15:04:05")
|
|
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO sessions (token, encryption_status, expires_at, created_at, updated_at)
|
|
VALUES (?, 'plain_text', ?, ?, ?)`,
|
|
"hash-startup-token", future, now, now)
|
|
|
|
count, err := store.encryptPlaintextSessions(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, count)
|
|
|
|
var raw map[string]any
|
|
db.Table("sessions").Take(&raw)
|
|
assert.Equal(t, "encrypted", raw["encryption_status"])
|
|
assert.Equal(t, encrypt.HashSHA256("hash-startup-token"), raw["token_hash"])
|
|
}
|
|
|
|
func TestEncryptPlaintextVirtualKeys_HashComputedDuringStartup(t *testing.T) {
|
|
store, db := setupEncryptionTestStore(t)
|
|
ctx := context.Background()
|
|
now := time.Now().UTC().Format("2006-01-02 15:04:05")
|
|
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO governance_virtual_keys (id, name, value, is_active, encryption_status, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, 'plain_text', ?, ?)`,
|
|
"vk-hash-startup", "hash-vk", "vk-hash-startup-value", true, now, now)
|
|
|
|
count, err := store.encryptPlaintextVirtualKeys(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, count)
|
|
|
|
var raw map[string]any
|
|
db.Table("governance_virtual_keys").Where("id = ?", "vk-hash-startup").Take(&raw)
|
|
assert.Equal(t, "encrypted", raw["encryption_status"])
|
|
assert.Equal(t, encrypt.HashSHA256("vk-hash-startup-value"), raw["value_hash"])
|
|
}
|
|
|
|
// ============================================================================
|
|
// MCP client env var connection string survives startup pass
|
|
// ============================================================================
|
|
|
|
func TestEncryptPlaintextMCPClients_EnvVarConnectionStringSurvivesStartup(t *testing.T) {
|
|
store, db := setupEncryptionTestStore(t)
|
|
ctx := context.Background()
|
|
now := time.Now().UTC().Format("2006-01-02 15:04:05")
|
|
|
|
t.Setenv("TEST_MCP_URL", "https://mcp-env.example.com/sse")
|
|
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO config_mcp_clients (client_id, name, connection_type, connection_string, headers_json, encryption_status, created_at, updated_at)
|
|
VALUES (?, ?, 'sse', ?, '{}', 'plain_text', ?, ?)`,
|
|
"mcp-env-startup", "env-startup-mcp", "env.TEST_MCP_URL", now, now)
|
|
|
|
count, err := store.encryptPlaintextMCPClients(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, count)
|
|
|
|
var raw map[string]any
|
|
db.Table("config_mcp_clients").Where("client_id = ?", "mcp-env-startup").Take(&raw)
|
|
assert.Equal(t, "encrypted", raw["encryption_status"])
|
|
assert.Equal(t, "env.TEST_MCP_URL", raw["connection_string"])
|
|
|
|
var found tables.TableMCPClient
|
|
require.NoError(t, db.Where("client_id = ?", "mcp-env-startup").First(&found).Error)
|
|
assert.Equal(t, "https://mcp-env.example.com/sse", found.ConnectionString.GetValue())
|
|
assert.True(t, found.ConnectionString.IsFromEnv())
|
|
}
|
|
|
|
// ============================================================================
|
|
// Already-encrypted rows skipped for non-key tables
|
|
// ============================================================================
|
|
|
|
func TestEncryptPlaintextRows_SkipsAlreadyEncryptedSessions(t *testing.T) {
|
|
store, db := setupEncryptionTestStore(t)
|
|
ctx := context.Background()
|
|
|
|
session := &tables.SessionsTable{
|
|
Token: "already-encrypted-session",
|
|
ExpiresAt: time.Now().Add(time.Hour),
|
|
}
|
|
require.NoError(t, db.Create(session).Error)
|
|
|
|
var rawBefore map[string]any
|
|
db.Table("sessions").Where("id = ?", session.ID).Take(&rawBefore)
|
|
encryptedBefore := rawBefore["token"]
|
|
|
|
err := store.EncryptPlaintextRows(ctx)
|
|
require.NoError(t, err)
|
|
|
|
var rawAfter map[string]any
|
|
db.Table("sessions").Where("id = ?", session.ID).Take(&rawAfter)
|
|
assert.Equal(t, encryptedBefore, rawAfter["token"])
|
|
}
|
|
|
|
func TestEncryptPlaintextRows_SkipsAlreadyEncryptedVirtualKeys(t *testing.T) {
|
|
store, db := setupEncryptionTestStore(t)
|
|
ctx := context.Background()
|
|
|
|
vk := &tables.TableVirtualKey{
|
|
ID: "vk-already-enc",
|
|
Name: "already-encrypted-vk",
|
|
Value: "vk-secret-already",
|
|
IsActive: true,
|
|
}
|
|
require.NoError(t, db.Create(vk).Error)
|
|
|
|
var rawBefore map[string]any
|
|
db.Table("governance_virtual_keys").Where("id = ?", "vk-already-enc").Take(&rawBefore)
|
|
encryptedBefore := rawBefore["value"]
|
|
|
|
err := store.EncryptPlaintextRows(ctx)
|
|
require.NoError(t, err)
|
|
|
|
var rawAfter map[string]any
|
|
db.Table("governance_virtual_keys").Where("id = ?", "vk-already-enc").Take(&rawAfter)
|
|
assert.Equal(t, encryptedBefore, rawAfter["value"])
|
|
}
|
|
|
|
// ============================================================================
|
|
// OAuthTokens with empty refresh token during startup pass
|
|
// ============================================================================
|
|
|
|
func TestEncryptPlaintextOAuthTokens_EmptyRefreshToken(t *testing.T) {
|
|
store, db := setupEncryptionTestStore(t)
|
|
ctx := context.Background()
|
|
now := time.Now().UTC().Format("2006-01-02 15:04:05")
|
|
future := time.Now().Add(time.Hour).UTC().Format("2006-01-02 15:04:05")
|
|
|
|
insertPlaintextRow(t, db,
|
|
`INSERT INTO oauth_tokens (id, access_token, refresh_token, token_type, encryption_status, expires_at, created_at, updated_at)
|
|
VALUES (?, ?, '', 'Bearer', 'plain_text', ?, ?, ?)`,
|
|
"tok-no-refresh", "access-only-startup", future, now, now)
|
|
|
|
count, err := store.encryptPlaintextOAuthTokens(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, count)
|
|
|
|
var raw map[string]any
|
|
db.Table("oauth_tokens").Where("id = ?", "tok-no-refresh").Take(&raw)
|
|
assert.Equal(t, "encrypted", raw["encryption_status"])
|
|
|
|
var found tables.TableOauthToken
|
|
require.NoError(t, db.First(&found, "id = ?", "tok-no-refresh").Error)
|
|
assert.Equal(t, "access-only-startup", found.AccessToken)
|
|
assert.Equal(t, "", found.RefreshToken)
|
|
}
|