From 9d04f2d760b54c17c020ca52334f063464c4fc22 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Fri, 21 Nov 2025 18:58:04 -0500 Subject: [PATCH] fix(secrets): Re-implements secrets injection Re-implements secrets injection. This functionality was lost when moving to the new runtime architecture. Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- pkg/runtime/runtime.go | 64 +++++++++---- pkg/runtime/runtime_test.go | 184 +++++++++++++++++++++++++++++++++--- 2 files changed, 218 insertions(+), 30 deletions(-) diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go index 5d0a1c984..1168d4e07 100644 --- a/pkg/runtime/runtime.go +++ b/pkg/runtime/runtime.go @@ -12,9 +12,10 @@ import ( "strings" "time" + secretsConfigType "github.com/windsorcli/cli/api/v1alpha1/secrets" "github.com/windsorcli/cli/pkg/runtime/config" "github.com/windsorcli/cli/pkg/runtime/env" - "github.com/windsorcli/cli/pkg/runtime/secrets" + secretsRuntime "github.com/windsorcli/cli/pkg/runtime/secrets" "github.com/windsorcli/cli/pkg/runtime/shell" "github.com/windsorcli/cli/pkg/runtime/tools" ) @@ -42,8 +43,8 @@ type Runtime struct { // SecretsProviders contains providers for Sops and 1Password secrets management SecretsProviders struct { - Sops secrets.SecretsProvider - Onepassword secrets.SecretsProvider + Sops secretsRuntime.SecretsProvider + Onepassword []secretsRuntime.SecretsProvider } // EnvPrinters contains environment printers for various providers and tools @@ -219,9 +220,9 @@ func (rt *Runtime) LoadEnvironment(decrypt bool) error { return fmt.Errorf("config handler not loaded") } + rt.initializeSecretsProviders() rt.initializeEnvPrinters() rt.initializeToolsManager() - rt.initializeSecretsProviders() if err := rt.initializeComponents(); err != nil { return fmt.Errorf("failed to initialize environment components: %w", err) @@ -405,12 +406,12 @@ func (rt *Runtime) initializeEnvPrinters() { rt.EnvPrinters.TerraformEnv = env.NewTerraformEnvPrinter(rt.Shell, rt.ConfigHandler) } if rt.EnvPrinters.WindsorEnv == nil { - secretsProviders := []secrets.SecretsProvider{} + secretsProviders := []secretsRuntime.SecretsProvider{} if rt.SecretsProviders.Sops != nil { secretsProviders = append(secretsProviders, rt.SecretsProviders.Sops) } if rt.SecretsProviders.Onepassword != nil { - secretsProviders = append(secretsProviders, rt.SecretsProviders.Onepassword) + secretsProviders = append(secretsProviders, rt.SecretsProviders.Onepassword...) } allEnvPrinters := rt.getAllEnvPrinters() rt.EnvPrinters.WindsorEnv = env.NewWindsorEnvPrinter(rt.Shell, rt.ConfigHandler, secretsProviders, allEnvPrinters) @@ -429,16 +430,44 @@ func (rt *Runtime) initializeToolsManager() { // initializeSecretsProviders initializes secrets providers based on current configuration settings. // The method sets up the SOPS provider if enabled with the context's config root path, and sets up -// the 1Password provider if enabled, using a mock in test scenarios. Providers are only initialized -// if not already present on the context. +// 1Password providers for each configured vault. Providers are only initialized if not already present. func (rt *Runtime) initializeSecretsProviders() { if rt.SecretsProviders.Sops == nil && rt.ConfigHandler.GetBool("secrets.sops.enabled", false) { configPath := rt.ConfigRoot - rt.SecretsProviders.Sops = secrets.NewSopsSecretsProvider(configPath, rt.Shell) - } - - if rt.SecretsProviders.Onepassword == nil && rt.ConfigHandler.GetBool("secrets.onepassword.enabled", false) { - rt.SecretsProviders.Onepassword = secrets.NewMockSecretsProvider(rt.Shell) + rt.SecretsProviders.Sops = secretsRuntime.NewSopsSecretsProvider(configPath, rt.Shell) + } + + if rt.SecretsProviders.Onepassword == nil { + vaultsValue := rt.ConfigHandler.Get("secrets.onepassword.vaults") + if vaultsValue != nil { + if vaultsMap, ok := vaultsValue.(map[string]any); ok { + rt.SecretsProviders.Onepassword = []secretsRuntime.SecretsProvider{} + for vaultID, vaultData := range vaultsMap { + if vaultMap, ok := vaultData.(map[string]any); ok { + vault := secretsConfigType.OnePasswordVault{ + ID: vaultID, + } + if url, ok := vaultMap["url"].(string); ok { + vault.URL = url + } + if name, ok := vaultMap["name"].(string); ok { + vault.Name = name + } + if id, ok := vaultMap["id"].(string); ok && id != "" { + vault.ID = id + } + + var provider secretsRuntime.SecretsProvider + if os.Getenv("OP_SERVICE_ACCOUNT_TOKEN") != "" { + provider = secretsRuntime.NewOnePasswordSDKSecretsProvider(vault, rt.Shell) + } else { + provider = secretsRuntime.NewOnePasswordCLISecretsProvider(vault, rt.Shell) + } + rt.SecretsProviders.Onepassword = append(rt.SecretsProviders.Onepassword, provider) + } + } + } + } } } @@ -467,9 +496,12 @@ func (rt *Runtime) initializeComponents() error { // loadSecrets loads secrets from configured secrets providers. // It attempts to load secrets from both SOPS and 1Password providers if they are available. func (rt *Runtime) loadSecrets() error { - providers := []secrets.SecretsProvider{ - rt.SecretsProviders.Sops, - rt.SecretsProviders.Onepassword, + providers := []secretsRuntime.SecretsProvider{} + if rt.SecretsProviders.Sops != nil { + providers = append(providers, rt.SecretsProviders.Sops) + } + if rt.SecretsProviders.Onepassword != nil { + providers = append(providers, rt.SecretsProviders.Onepassword...) } for _, provider := range providers { diff --git a/pkg/runtime/runtime_test.go b/pkg/runtime/runtime_test.go index afb97933c..f657b2a23 100644 --- a/pkg/runtime/runtime_test.go +++ b/pkg/runtime/runtime_test.go @@ -394,7 +394,7 @@ func TestRuntime_NewRuntime(t *testing.T) { }, } rtOpts[0].SecretsProviders.Sops = mockSopsProvider - rtOpts[0].SecretsProviders.Onepassword = mockOnepasswordProvider + rtOpts[0].SecretsProviders.Onepassword = []secrets.SecretsProvider{mockOnepasswordProvider} rtOpts[0].EnvPrinters.AwsEnv = mockAwsEnv rtOpts[0].EnvPrinters.AzureEnv = mockAzureEnv rtOpts[0].EnvPrinters.DockerEnv = mockDockerEnv @@ -436,7 +436,7 @@ func TestRuntime_NewRuntime(t *testing.T) { t.Error("Expected Sops provider to be set") } - if rt.SecretsProviders.Onepassword != mockOnepasswordProvider { + if len(rt.SecretsProviders.Onepassword) != 1 || rt.SecretsProviders.Onepassword[0] != mockOnepasswordProvider { t.Error("Expected Onepassword provider to be set") } @@ -625,7 +625,7 @@ func TestRuntime_LoadEnvironment(t *testing.T) { mockOnepasswordProvider := secrets.NewMockSecretsProvider(mocks.Shell) rt.SecretsProviders.Sops = mockSopsProvider - rt.SecretsProviders.Onepassword = mockOnepasswordProvider + rt.SecretsProviders.Onepassword = []secrets.SecretsProvider{mockOnepasswordProvider} // When LoadEnvironment is called with secrets enabled err := rt.LoadEnvironment(true) @@ -2306,7 +2306,7 @@ func TestRuntime_loadSecrets(t *testing.T) { mockOnepasswordProvider := secrets.NewMockSecretsProvider(mocks.Shell) rt.SecretsProviders.Sops = mockSopsProvider - rt.SecretsProviders.Onepassword = mockOnepasswordProvider + rt.SecretsProviders.Onepassword = []secrets.SecretsProvider{mockOnepasswordProvider} // When loadSecrets is called err := rt.loadSecrets() @@ -2402,27 +2402,35 @@ func TestRuntime_initializeSecretsProviders(t *testing.T) { } }) - t.Run("InitializesOnepasswordProviderWhenEnabled", func(t *testing.T) { - // Given a runtime with 1Password enabled in config + t.Run("InitializesOnepasswordProviderWhenVaultsConfigured", func(t *testing.T) { + // Given a runtime with 1Password vaults configured mocks := setupRuntimeMocks(t) rt := mocks.Runtime mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) - mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { - if key == "secrets.onepassword.enabled" { - return true + mockConfigHandler.GetFunc = func(key string) any { + if key == "secrets.onepassword.vaults" { + return map[string]any{ + "personal": map[string]any{ + "url": "my.1password.com", + "name": "Personal", + }, + } } - return false + return nil } // When initializeSecretsProviders is called rt.initializeSecretsProviders() - // Then 1Password provider should be initialized + // Then 1Password provider should be initialized for each vault - if rt.SecretsProviders.Onepassword == nil { + if len(rt.SecretsProviders.Onepassword) == 0 { t.Error("Expected 1Password provider to be initialized") } + if len(rt.SecretsProviders.Onepassword) != 1 { + t.Errorf("Expected 1 provider, got %d", len(rt.SecretsProviders.Onepassword)) + } }) t.Run("SkipsProvidersWhenDisabled", func(t *testing.T) { @@ -2444,7 +2452,7 @@ func TestRuntime_initializeSecretsProviders(t *testing.T) { t.Error("Expected SOPS provider to be nil when disabled") } - if rt.SecretsProviders.Onepassword != nil { + if len(rt.SecretsProviders.Onepassword) > 0 { t.Error("Expected 1Password provider to be nil when disabled") } }) @@ -2473,6 +2481,154 @@ func TestRuntime_initializeSecretsProviders(t *testing.T) { t.Error("Expected existing provider to be preserved") } }) + + t.Run("InitializesMultipleVaults", func(t *testing.T) { + // Given a runtime with multiple 1Password vaults configured + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.GetFunc = func(key string) any { + if key == "secrets.onepassword.vaults" { + return map[string]any{ + "personal": map[string]any{ + "url": "my.1password.com", + "name": "Personal", + }, + "work": map[string]any{ + "url": "company.1password.com", + "name": "Work", + }, + } + } + return nil + } + + // When initializeSecretsProviders is called + rt.initializeSecretsProviders() + + // Then multiple providers should be initialized + if len(rt.SecretsProviders.Onepassword) == 0 { + t.Error("Expected 1Password providers to be initialized") + } + if len(rt.SecretsProviders.Onepassword) != 2 { + t.Errorf("Expected 2 providers, got %d", len(rt.SecretsProviders.Onepassword)) + } + }) + + t.Run("HandlesEmptyVaultsMap", func(t *testing.T) { + // Given a runtime with empty vaults map + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.GetFunc = func(key string) any { + if key == "secrets.onepassword.vaults" { + return map[string]any{} + } + return nil + } + + // When initializeSecretsProviders is called + rt.initializeSecretsProviders() + + // Then no providers should be initialized + if len(rt.SecretsProviders.Onepassword) > 0 { + t.Error("Expected no providers to be initialized for empty vaults map") + } + }) + + t.Run("HandlesInvalidVaultData", func(t *testing.T) { + // Given a runtime with invalid vault data structure + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.GetFunc = func(key string) any { + if key == "secrets.onepassword.vaults" { + return map[string]any{ + "personal": "not-a-map", + } + } + return nil + } + + // When initializeSecretsProviders is called + rt.initializeSecretsProviders() + + // Then no providers should be initialized for invalid data + if len(rt.SecretsProviders.Onepassword) > 0 { + t.Error("Expected no providers to be initialized for invalid vault data") + } + }) + + t.Run("HandlesVaultWithExplicitID", func(t *testing.T) { + // Given a runtime with vault that has explicit ID field + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.GetFunc = func(key string) any { + if key == "secrets.onepassword.vaults" { + return map[string]any{ + "personal": map[string]any{ + "id": "custom-vault-id", + "url": "my.1password.com", + "name": "Personal", + }, + } + } + return nil + } + + // When initializeSecretsProviders is called + rt.initializeSecretsProviders() + + // Then provider should be initialized with explicit ID + if len(rt.SecretsProviders.Onepassword) == 0 { + t.Error("Expected 1Password provider to be initialized") + } + if len(rt.SecretsProviders.Onepassword) != 1 { + t.Errorf("Expected 1 provider, got %d", len(rt.SecretsProviders.Onepassword)) + } + }) + + t.Run("InitializesProvidersBeforeWindsorEnv", func(t *testing.T) { + // Given a runtime with vaults configured + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.GetFunc = func(key string) any { + if key == "secrets.onepassword.vaults" { + return map[string]any{ + "personal": map[string]any{ + "url": "my.1password.com", + "name": "Personal", + }, + } + } + return nil + } + + // When LoadEnvironment is called + err := rt.LoadEnvironment(false) + + // Then no error should occur + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + // And providers should be initialized + if len(rt.SecretsProviders.Onepassword) == 0 { + t.Error("Expected 1Password providers to be initialized") + } + + // And WindsorEnv should be initialized with providers + if rt.EnvPrinters.WindsorEnv == nil { + t.Error("Expected WindsorEnv to be initialized") + } + }) } func TestRuntime_initializeComponents_EdgeCases(t *testing.T) { @@ -2736,7 +2892,7 @@ func TestRuntime_initializeEnvPrinters(t *testing.T) { mockSopsProvider := secrets.NewMockSecretsProvider(mocks.Shell) mockOnepasswordProvider := secrets.NewMockSecretsProvider(mocks.Shell) rt.SecretsProviders.Sops = mockSopsProvider - rt.SecretsProviders.Onepassword = mockOnepasswordProvider + rt.SecretsProviders.Onepassword = []secrets.SecretsProvider{mockOnepasswordProvider} // When initializeEnvPrinters is called rt.initializeEnvPrinters()