diff options
Diffstat (limited to 'test/kms/openbao_integration_test.go')
| -rw-r--r-- | test/kms/openbao_integration_test.go | 598 |
1 files changed, 598 insertions, 0 deletions
diff --git a/test/kms/openbao_integration_test.go b/test/kms/openbao_integration_test.go new file mode 100644 index 000000000..d4e62ed4d --- /dev/null +++ b/test/kms/openbao_integration_test.go @@ -0,0 +1,598 @@ +package kms_test + +import ( + "context" + "fmt" + "os" + "os/exec" + "strings" + "testing" + "time" + + "github.com/hashicorp/vault/api" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/seaweedfs/seaweedfs/weed/glog" + "github.com/seaweedfs/seaweedfs/weed/kms" + _ "github.com/seaweedfs/seaweedfs/weed/kms/openbao" +) + +const ( + OpenBaoAddress = "http://127.0.0.1:8200" + OpenBaoToken = "root-token-for-testing" + TransitPath = "transit" +) + +// Test configuration for OpenBao KMS provider +type testConfig struct { + config map[string]interface{} +} + +func (c *testConfig) GetString(key string) string { + if val, ok := c.config[key]; ok { + if str, ok := val.(string); ok { + return str + } + } + return "" +} + +func (c *testConfig) GetBool(key string) bool { + if val, ok := c.config[key]; ok { + if b, ok := val.(bool); ok { + return b + } + } + return false +} + +func (c *testConfig) GetInt(key string) int { + if val, ok := c.config[key]; ok { + if i, ok := val.(int); ok { + return i + } + if f, ok := val.(float64); ok { + return int(f) + } + } + return 0 +} + +func (c *testConfig) GetStringSlice(key string) []string { + if val, ok := c.config[key]; ok { + if slice, ok := val.([]string); ok { + return slice + } + } + return nil +} + +func (c *testConfig) SetDefault(key string, value interface{}) { + if c.config == nil { + c.config = make(map[string]interface{}) + } + if _, exists := c.config[key]; !exists { + c.config[key] = value + } +} + +// setupOpenBao starts OpenBao in development mode for testing +func setupOpenBao(t *testing.T) (*exec.Cmd, func()) { + // Check if OpenBao is running in Docker (via make dev-openbao) + client, err := api.NewClient(&api.Config{Address: OpenBaoAddress}) + if err == nil { + client.SetToken(OpenBaoToken) + _, err = client.Sys().Health() + if err == nil { + glog.V(1).Infof("Using existing OpenBao server at %s", OpenBaoAddress) + // Return dummy command and cleanup function for existing server + return nil, func() {} + } + } + + // Check if OpenBao binary is available for starting locally + _, err = exec.LookPath("bao") + if err != nil { + t.Skip("OpenBao not running and bao binary not found. Run 'cd test/kms && make dev-openbao' first") + } + + // Start OpenBao in dev mode + cmd := exec.Command("bao", "server", "-dev", "-dev-root-token-id="+OpenBaoToken, "-dev-listen-address=127.0.0.1:8200") + cmd.Env = append(os.Environ(), "BAO_DEV_ROOT_TOKEN_ID="+OpenBaoToken) + + // Capture output for debugging + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + err = cmd.Start() + require.NoError(t, err, "Failed to start OpenBao server") + + // Wait for OpenBao to be ready + client, err = api.NewClient(&api.Config{Address: OpenBaoAddress}) + require.NoError(t, err) + client.SetToken(OpenBaoToken) + + // Wait up to 30 seconds for OpenBao to be ready + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + for { + select { + case <-ctx.Done(): + cmd.Process.Kill() + t.Fatal("Timeout waiting for OpenBao to start") + default: + // Try to check health + resp, err := client.Sys().Health() + if err == nil && resp.Initialized { + glog.V(1).Infof("OpenBao server ready") + goto ready + } + time.Sleep(500 * time.Millisecond) + } + } + +ready: + // Setup cleanup function + cleanup := func() { + if cmd != nil && cmd.Process != nil { + glog.V(1).Infof("Stopping OpenBao server") + cmd.Process.Kill() + cmd.Wait() + } + } + + return cmd, cleanup +} + +// setupTransitEngine enables and configures the transit secrets engine +func setupTransitEngine(t *testing.T) { + client, err := api.NewClient(&api.Config{Address: OpenBaoAddress}) + require.NoError(t, err) + client.SetToken(OpenBaoToken) + + // Enable transit secrets engine + err = client.Sys().Mount(TransitPath, &api.MountInput{ + Type: "transit", + Description: "Transit engine for KMS testing", + }) + if err != nil && !strings.Contains(err.Error(), "path is already in use") { + require.NoError(t, err, "Failed to enable transit engine") + } + + // Create test encryption keys + testKeys := []string{"test-key-1", "test-key-2", "seaweedfs-test-key"} + + for _, keyName := range testKeys { + keyData := map[string]interface{}{ + "type": "aes256-gcm96", + } + + path := fmt.Sprintf("%s/keys/%s", TransitPath, keyName) + _, err = client.Logical().Write(path, keyData) + if err != nil && !strings.Contains(err.Error(), "key already exists") { + require.NoError(t, err, "Failed to create test key %s", keyName) + } + + glog.V(2).Infof("Created/verified test key: %s", keyName) + } +} + +func TestOpenBaoKMSProvider_Integration(t *testing.T) { + // Start OpenBao server + _, cleanup := setupOpenBao(t) + defer cleanup() + + // Setup transit engine and keys + setupTransitEngine(t) + + t.Run("CreateProvider", func(t *testing.T) { + config := &testConfig{ + config: map[string]interface{}{ + "address": OpenBaoAddress, + "token": OpenBaoToken, + "transit_path": TransitPath, + }, + } + + provider, err := kms.GetProvider("openbao", config) + require.NoError(t, err) + require.NotNil(t, provider) + + defer provider.Close() + }) + + t.Run("ProviderRegistration", func(t *testing.T) { + // Test that the provider is registered + providers := kms.ListProviders() + assert.Contains(t, providers, "openbao") + assert.Contains(t, providers, "vault") // Compatibility alias + }) + + t.Run("GenerateDataKey", func(t *testing.T) { + config := &testConfig{ + config: map[string]interface{}{ + "address": OpenBaoAddress, + "token": OpenBaoToken, + "transit_path": TransitPath, + }, + } + + provider, err := kms.GetProvider("openbao", config) + require.NoError(t, err) + defer provider.Close() + + ctx := context.Background() + req := &kms.GenerateDataKeyRequest{ + KeyID: "test-key-1", + KeySpec: kms.KeySpecAES256, + EncryptionContext: map[string]string{ + "test": "context", + "env": "integration", + }, + } + + resp, err := provider.GenerateDataKey(ctx, req) + require.NoError(t, err) + require.NotNil(t, resp) + + assert.Equal(t, "test-key-1", resp.KeyID) + assert.Len(t, resp.Plaintext, 32) // 256 bits + assert.NotEmpty(t, resp.CiphertextBlob) + + // Verify the response is in standardized envelope format + envelope, err := kms.ParseEnvelope(resp.CiphertextBlob) + assert.NoError(t, err) + assert.Equal(t, "openbao", envelope.Provider) + assert.Equal(t, "test-key-1", envelope.KeyID) + assert.True(t, strings.HasPrefix(envelope.Ciphertext, "vault:")) // Raw OpenBao format inside envelope + }) + + t.Run("DecryptDataKey", func(t *testing.T) { + config := &testConfig{ + config: map[string]interface{}{ + "address": OpenBaoAddress, + "token": OpenBaoToken, + "transit_path": TransitPath, + }, + } + + provider, err := kms.GetProvider("openbao", config) + require.NoError(t, err) + defer provider.Close() + + ctx := context.Background() + + // First generate a data key + genReq := &kms.GenerateDataKeyRequest{ + KeyID: "test-key-1", + KeySpec: kms.KeySpecAES256, + EncryptionContext: map[string]string{ + "test": "decrypt", + "env": "integration", + }, + } + + genResp, err := provider.GenerateDataKey(ctx, genReq) + require.NoError(t, err) + + // Now decrypt it + decReq := &kms.DecryptRequest{ + CiphertextBlob: genResp.CiphertextBlob, + EncryptionContext: map[string]string{ + "openbao:key:name": "test-key-1", + "test": "decrypt", + "env": "integration", + }, + } + + decResp, err := provider.Decrypt(ctx, decReq) + require.NoError(t, err) + require.NotNil(t, decResp) + + assert.Equal(t, "test-key-1", decResp.KeyID) + assert.Equal(t, genResp.Plaintext, decResp.Plaintext) + }) + + t.Run("DescribeKey", func(t *testing.T) { + config := &testConfig{ + config: map[string]interface{}{ + "address": OpenBaoAddress, + "token": OpenBaoToken, + "transit_path": TransitPath, + }, + } + + provider, err := kms.GetProvider("openbao", config) + require.NoError(t, err) + defer provider.Close() + + ctx := context.Background() + req := &kms.DescribeKeyRequest{ + KeyID: "test-key-1", + } + + resp, err := provider.DescribeKey(ctx, req) + require.NoError(t, err) + require.NotNil(t, resp) + + assert.Equal(t, "test-key-1", resp.KeyID) + assert.Contains(t, resp.ARN, "openbao:") + assert.Equal(t, kms.KeyStateEnabled, resp.KeyState) + assert.Equal(t, kms.KeyUsageEncryptDecrypt, resp.KeyUsage) + }) + + t.Run("NonExistentKey", func(t *testing.T) { + config := &testConfig{ + config: map[string]interface{}{ + "address": OpenBaoAddress, + "token": OpenBaoToken, + "transit_path": TransitPath, + }, + } + + provider, err := kms.GetProvider("openbao", config) + require.NoError(t, err) + defer provider.Close() + + ctx := context.Background() + req := &kms.DescribeKeyRequest{ + KeyID: "non-existent-key", + } + + _, err = provider.DescribeKey(ctx, req) + require.Error(t, err) + + kmsErr, ok := err.(*kms.KMSError) + require.True(t, ok) + assert.Equal(t, kms.ErrCodeNotFoundException, kmsErr.Code) + }) + + t.Run("MultipleKeys", func(t *testing.T) { + config := &testConfig{ + config: map[string]interface{}{ + "address": OpenBaoAddress, + "token": OpenBaoToken, + "transit_path": TransitPath, + }, + } + + provider, err := kms.GetProvider("openbao", config) + require.NoError(t, err) + defer provider.Close() + + ctx := context.Background() + + // Test with multiple keys + testKeys := []string{"test-key-1", "test-key-2", "seaweedfs-test-key"} + + for _, keyName := range testKeys { + t.Run(fmt.Sprintf("Key_%s", keyName), func(t *testing.T) { + // Generate data key + genReq := &kms.GenerateDataKeyRequest{ + KeyID: keyName, + KeySpec: kms.KeySpecAES256, + EncryptionContext: map[string]string{ + "key": keyName, + }, + } + + genResp, err := provider.GenerateDataKey(ctx, genReq) + require.NoError(t, err) + assert.Equal(t, keyName, genResp.KeyID) + + // Decrypt data key + decReq := &kms.DecryptRequest{ + CiphertextBlob: genResp.CiphertextBlob, + EncryptionContext: map[string]string{ + "openbao:key:name": keyName, + "key": keyName, + }, + } + + decResp, err := provider.Decrypt(ctx, decReq) + require.NoError(t, err) + assert.Equal(t, genResp.Plaintext, decResp.Plaintext) + }) + } + }) +} + +func TestOpenBaoKMSProvider_ErrorHandling(t *testing.T) { + // Start OpenBao server + _, cleanup := setupOpenBao(t) + defer cleanup() + + setupTransitEngine(t) + + t.Run("InvalidToken", func(t *testing.T) { + t.Skip("Skipping invalid token test - OpenBao dev mode may be too permissive") + + config := &testConfig{ + config: map[string]interface{}{ + "address": OpenBaoAddress, + "token": "invalid-token", + "transit_path": TransitPath, + }, + } + + provider, err := kms.GetProvider("openbao", config) + require.NoError(t, err) // Provider creation doesn't validate token + defer provider.Close() + + ctx := context.Background() + req := &kms.GenerateDataKeyRequest{ + KeyID: "test-key-1", + KeySpec: kms.KeySpecAES256, + } + + _, err = provider.GenerateDataKey(ctx, req) + require.Error(t, err) + + // Check that it's a KMS error (could be access denied or other auth error) + kmsErr, ok := err.(*kms.KMSError) + require.True(t, ok, "Expected KMSError but got: %T", err) + // OpenBao might return different error codes for invalid tokens + assert.Contains(t, []string{kms.ErrCodeAccessDenied, kms.ErrCodeKMSInternalFailure}, kmsErr.Code) + }) + +} + +func TestKMSManager_WithOpenBao(t *testing.T) { + // Start OpenBao server + _, cleanup := setupOpenBao(t) + defer cleanup() + + setupTransitEngine(t) + + t.Run("KMSManagerIntegration", func(t *testing.T) { + manager := kms.InitializeKMSManager() + + // Add OpenBao provider to manager + kmsConfig := &kms.KMSConfig{ + Provider: "openbao", + Config: map[string]interface{}{ + "address": OpenBaoAddress, + "token": OpenBaoToken, + "transit_path": TransitPath, + }, + CacheEnabled: true, + CacheTTL: time.Hour, + } + + err := manager.AddKMSProvider("openbao-test", kmsConfig) + require.NoError(t, err) + + // Set as default provider + err = manager.SetDefaultKMSProvider("openbao-test") + require.NoError(t, err) + + // Test bucket-specific assignment + err = manager.SetBucketKMSProvider("test-bucket", "openbao-test") + require.NoError(t, err) + + // Test key operations through manager + ctx := context.Background() + resp, err := manager.GenerateDataKeyForBucket(ctx, "test-bucket", "test-key-1", kms.KeySpecAES256, map[string]string{ + "bucket": "test-bucket", + }) + require.NoError(t, err) + require.NotNil(t, resp) + + assert.Equal(t, "test-key-1", resp.KeyID) + assert.Len(t, resp.Plaintext, 32) + + // Test decryption through manager + decResp, err := manager.DecryptForBucket(ctx, "test-bucket", resp.CiphertextBlob, map[string]string{ + "bucket": "test-bucket", + }) + require.NoError(t, err) + assert.Equal(t, resp.Plaintext, decResp.Plaintext) + + // Test health check + health := manager.GetKMSHealth(ctx) + assert.Contains(t, health, "openbao-test") + assert.NoError(t, health["openbao-test"]) // Should be healthy + + // Cleanup + manager.Close() + }) +} + +// Benchmark tests for performance +func BenchmarkOpenBaoKMS_GenerateDataKey(b *testing.B) { + if testing.Short() { + b.Skip("Skipping benchmark in short mode") + } + + // Start OpenBao server + _, cleanup := setupOpenBao(&testing.T{}) + defer cleanup() + + setupTransitEngine(&testing.T{}) + + config := &testConfig{ + config: map[string]interface{}{ + "address": OpenBaoAddress, + "token": OpenBaoToken, + "transit_path": TransitPath, + }, + } + + provider, err := kms.GetProvider("openbao", config) + if err != nil { + b.Fatal(err) + } + defer provider.Close() + + ctx := context.Background() + req := &kms.GenerateDataKeyRequest{ + KeyID: "test-key-1", + KeySpec: kms.KeySpecAES256, + } + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _, err := provider.GenerateDataKey(ctx, req) + if err != nil { + b.Fatal(err) + } + } + }) +} + +func BenchmarkOpenBaoKMS_Decrypt(b *testing.B) { + if testing.Short() { + b.Skip("Skipping benchmark in short mode") + } + + // Start OpenBao server + _, cleanup := setupOpenBao(&testing.T{}) + defer cleanup() + + setupTransitEngine(&testing.T{}) + + config := &testConfig{ + config: map[string]interface{}{ + "address": OpenBaoAddress, + "token": OpenBaoToken, + "transit_path": TransitPath, + }, + } + + provider, err := kms.GetProvider("openbao", config) + if err != nil { + b.Fatal(err) + } + defer provider.Close() + + ctx := context.Background() + + // Generate a data key for decryption testing + genResp, err := provider.GenerateDataKey(ctx, &kms.GenerateDataKeyRequest{ + KeyID: "test-key-1", + KeySpec: kms.KeySpecAES256, + }) + if err != nil { + b.Fatal(err) + } + + decReq := &kms.DecryptRequest{ + CiphertextBlob: genResp.CiphertextBlob, + EncryptionContext: map[string]string{ + "openbao:key:name": "test-key-1", + }, + } + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _, err := provider.Decrypt(ctx, decReq) + if err != nil { + b.Fatal(err) + } + } + }) +} |
