From 17c8030e3f886c977c62421bd94978d8b87c017e Mon Sep 17 00:00:00 2001 From: Ryan VanGundy Date: Fri, 11 Apr 2025 22:23:54 -0400 Subject: [PATCH 1/2] Add op_sdk_secrets_provider --- .github/renovate.json | 7 + .goreleaser.yaml | 1 + .vscode/launch.json | 2 +- aqua.yaml | 4 +- cmd/env.go | 5 - cmd/env_test.go | 42 +- cmd/install_test.go | 2 +- go.mod | 11 +- go.sum | 99 ++- pkg/config/config_handler_test.go | 2 +- pkg/config/mock_config_handler_test.go | 4 +- pkg/controller/controller_test.go | 4 +- pkg/controller/mock_controller.go | 2 +- pkg/controller/real_controller.go | 12 +- pkg/env/windsor_env.go | 24 +- pkg/env/windsor_env_test.go | 409 +++++++++++- pkg/secrets/mock_secrets_provider.go | 13 +- pkg/secrets/mock_secrets_provider_test.go | 20 +- pkg/secrets/op_cli_secrets_provider.go | 33 +- pkg/secrets/op_cli_secrets_provider_test.go | 466 ++++++++------ pkg/secrets/op_sdk_secrets_provider.go | 124 ++++ pkg/secrets/op_sdk_secrets_provider_test.go | 652 ++++++++++++++++++++ pkg/secrets/secrets_provider.go | 56 +- pkg/secrets/secrets_provider_test.go | 48 +- pkg/secrets/shims.go | 16 + pkg/secrets/sops_secrets_provider.go | 32 +- 26 files changed, 1685 insertions(+), 405 deletions(-) create mode 100644 pkg/secrets/op_sdk_secrets_provider.go create mode 100644 pkg/secrets/op_sdk_secrets_provider_test.go diff --git a/.github/renovate.json b/.github/renovate.json index fa25065c8..8c87c6556 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -6,6 +6,13 @@ ":dependencyDashboard" ], "packageRules": [ + { + "description": "Pin SOPS to v3.9.0", + "matchPackageNames": [ + "getsops/sops" + ], + "allowedVersions": "3.9.0" + }, { "description": "Pin GitHub Actions to specific commit SHAs", "matchManagers": [ diff --git a/.goreleaser.yaml b/.goreleaser.yaml index a11f84ecd..f33bcff0e 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -23,6 +23,7 @@ builds: - amd64 ldflags: - "-X 'github.com/{{ .Env.GITHUB_REPOSITORY }}/cmd.version={{ .Version }}'" + - "-X 'github.com/{{ .Env.GITHUB_REPOSITORY }}/pkg/secrets.version={{ .Version }}'" - "-X 'github.com/{{ .Env.GITHUB_REPOSITORY }}/cmd.commitSHA={{ .Env.GITHUB_SHA }}'" # Archive configuration diff --git a/.vscode/launch.json b/.vscode/launch.json index f1dc4ad1e..3b534b9c9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -14,7 +14,7 @@ "request": "launch", "mode": "auto", "program": "${workspaceFolder}/cmd/windsor/main.go", - "args": ["env", "--verbose"], + "args": ["env", "--verbose", "--decrypt"], "env": { "WINDSOR_SESSION_TOKEN": "local" } diff --git a/aqua.yaml b/aqua.yaml index 1116ed07b..9dadec14c 100644 --- a/aqua.yaml +++ b/aqua.yaml @@ -6,7 +6,7 @@ # enabled: true # require_checksum: true # supported_envs: -# - all +# - all registries: - type: standard ref: v4.344.0 # renovate: depName=aquaproj/aqua-registry @@ -18,7 +18,7 @@ packages: - name: kubernetes/kubectl@v1.32.3 - name: go-task/task@v3.42.1 - name: golang/go@go1.24.2 -- name: getsops/sops@v3.10.1 +- name: getsops/sops@v3.9.0 - name: abiosoft/colima@v0.8.1 - name: lima-vm/lima@v1.0.7 - name: docker/cli@v27.4.1 diff --git a/cmd/env.go b/cmd/env.go index 528eb7fbc..9e00503ef 100644 --- a/cmd/env.go +++ b/cmd/env.go @@ -93,11 +93,6 @@ var envCmd = &cobra.Command{ return nil } - // Set the environment variables internally in the process - if err := controller.SetEnvironmentVariables(); err != nil { - return fmt.Errorf("Error setting environment variables: %w", err) - } - envPrinters := controller.ResolveAllEnvPrinters() if len(envPrinters) == 0 { if verbose { diff --git a/cmd/env_test.go b/cmd/env_test.go index 6425c2f5d..26531ea3b 100644 --- a/cmd/env_test.go +++ b/cmd/env_test.go @@ -61,7 +61,7 @@ func setupSafeMocks() *TestMocks { } // Set up mock secrets provider - mockSecretsProvider := secrets.NewMockSecretsProvider() + mockSecretsProvider := secrets.NewMockSecretsProvider(injector) mockController.ResolveAllSecretsProvidersFunc = func() []secrets.SecretsProvider { return []secrets.SecretsProvider{mockSecretsProvider} } @@ -447,31 +447,6 @@ func TestEnvCmd(t *testing.T) { } }) - t.Run("ErrorResolvingAllEnvPrinters", func(t *testing.T) { - defer resetRootCmd() - - // Setup safe mocks - mocks := setupSafeMocks() - - // Return empty list for env printers - mocks.Controller.ResolveAllEnvPrintersFunc = func() []env.EnvPrinter { - return []env.EnvPrinter{} - } - - // When the env command is executed with verbose flag - rootCmd.SetArgs([]string{"env", "--verbose"}) - err := Execute(mocks.Controller) - - // Then check the error contents - if err == nil { - t.Fatalf("Expected an error, got nil") - } - expectedError := "Error resolving environment printers: no printers returned" - if err.Error() != expectedError { - t.Fatalf("Expected error %q, got %v", expectedError, err) - } - }) - t.Run("PrintError", func(t *testing.T) { defer resetRootCmd() @@ -903,7 +878,7 @@ func TestEnvCmd(t *testing.T) { } // On any subsequent calls, add a provider - mockProvider := secrets.NewMockSecretsProvider() + mockProvider := secrets.NewMockSecretsProvider(mocks.Injector) return []secrets.SecretsProvider{mockProvider} } @@ -1231,18 +1206,13 @@ func TestEnvCmd(t *testing.T) { return fmt.Errorf("error setting environment variables") } - // Run the env command + // Run the env command without verbose flag rootCmd.SetArgs([]string{"env"}) err := Execute(mocks.Controller) - // Expect an error - if err == nil { - t.Fatalf("Expected an error, got nil") - } - - expectedError := "Error setting environment variables: error setting environment variables" - if err.Error() != expectedError { - t.Errorf("Expected error %q, got %q", expectedError, err.Error()) + // Then the error should be nil and no output should be produced + if err != nil { + t.Fatalf("Expected error nil, got %v", err) } }) diff --git a/cmd/install_test.go b/cmd/install_test.go index 4dbdc86c3..3fcfaf5a0 100644 --- a/cmd/install_test.go +++ b/cmd/install_test.go @@ -51,7 +51,7 @@ func setupMockInstallCmdComponents(optionalInjector ...di.Injector) InstallCmdCo injector.Register("configHandler", configHandler) // Setup mock secrets provider - secretsProvider := secrets.NewMockSecretsProvider() + secretsProvider := secrets.NewMockSecretsProvider(injector) controller.ResolveAllSecretsProvidersFunc = func() []secrets.SecretsProvider { return []secrets.SecretsProvider{secretsProvider} } diff --git a/go.mod b/go.mod index f2fc794c8..5cb12110a 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/windsorcli/cli go 1.23.4 require ( + github.com/1password/onepassword-sdk-go v0.2.1 github.com/abiosoft/colima v0.8.1 github.com/aws/smithy-go v1.22.3 github.com/briandowns/spinner v1.23.2 @@ -11,7 +12,7 @@ require ( github.com/fluxcd/pkg/apis/kustomize v1.9.0 github.com/fluxcd/pkg/apis/meta v1.10.0 github.com/fluxcd/source-controller/api v1.5.0 - github.com/getsops/sops/v3 v3.10.1 + github.com/getsops/sops/v3 v3.9.0 github.com/goccy/go-yaml v1.17.1 github.com/google/go-jsonnet v0.20.0 github.com/hashicorp/hcl/v2 v2.23.0 @@ -38,7 +39,6 @@ require ( cloud.google.com/go/monitoring v1.24.1 // indirect cloud.google.com/go/storage v1.51.0 // indirect filippo.io/age v1.2.1 // indirect - filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect @@ -81,9 +81,11 @@ require ( github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect + github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect + github.com/extism/go-sdk v1.7.0 // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fluxcd/pkg/apis/acl v0.6.0 // indirect @@ -96,6 +98,7 @@ require ( github.com/go-openapi/jsonpointer v0.21.1 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.1 // indirect + github.com/gobwas/glob v0.2.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/golang/protobuf v1.5.4 // indirect @@ -118,6 +121,7 @@ require ( github.com/hashicorp/go-sockaddr v1.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/vault/api v1.16.0 // indirect + github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -141,6 +145,8 @@ require ( github.com/ryanuber/go-glob v1.0.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/pflag v1.0.6 // indirect + github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect + github.com/tetratelabs/wazero v1.8.2 // indirect github.com/urfave/cli v1.22.16 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect @@ -153,6 +159,7 @@ require ( go.opentelemetry.io/otel/sdk v1.35.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect golang.org/x/mod v0.24.0 // indirect golang.org/x/net v0.39.0 // indirect diff --git a/go.sum b/go.sum index ea77ead34..645d3b1a7 100644 --- a/go.sum +++ b/go.sum @@ -24,28 +24,26 @@ cloud.google.com/go/storage v1.51.0 h1:ZVZ11zCiD7b3k+cH5lQs/qcNaoSz3U9I0jgwVzqDl cloud.google.com/go/storage v1.51.0/go.mod h1:YEJfu/Ki3i5oHC/7jyTgsGZwdQ8P9hqMqvpi5kRKGgc= cloud.google.com/go/trace v1.11.5 h1:CALS1loyxJMnRiCwZSpdf8ac7iCsjreMxFD2WGxzzHU= cloud.google.com/go/trace v1.11.5/go.mod h1:TwblCcqNInriu5/qzaeYEIH7wzUcchSdeY2l5wL3Eec= -dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= -dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= filippo.io/age v1.2.1 h1:X0TZjehAZylOIj4DubWYU1vWQxv9bJpo+Uu2/LGhi1o= filippo.io/age v1.2.1/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004= -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/1password/onepassword-sdk-go v0.2.1 h1:wwJmjR3UrwYxgAmNpKZ/mHOgFYCz6aQx7NxQ2YCFOL8= +github.com/1password/onepassword-sdk-go v0.2.1/go.mod h1:R+3/jgPZRbfuXrMCqrl3NM46MMbpc4Zue5S5KRv6yC8= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 h1:F0gBpfdPLGsw+nsgk6aqqkZS1jiixa5WwFe3fk/T3Ys= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2/go.mod h1:SqINnQ9lVVdRlyC8cd1lCI0SdX4n2paeABd2K8ggfnE= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.0 h1:Bg8m3nq/X1DeePkAbCfb6ml6F3F0IunEhE8TMh+lY48= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.0/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 h1:Wgf5rZba3YZqeTNJPtvqZoBu1sBN/L4sry+u2U3Y75w= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1/go.mod h1:xxCBG/f/4Vbmh2XQJBsOmNdxWUY5j/s27jujKPbQf14= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww= -github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= -github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= @@ -75,18 +73,12 @@ github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38y github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14= -github.com/aws/aws-sdk-go-v2/config v1.29.13 h1:RgdPqWoE8nPpIekpVpDJsBckbqT4Liiaq9f35pbTh1Y= -github.com/aws/aws-sdk-go-v2/config v1.29.13/go.mod h1:NI28qs/IOUIRhsR7GQ/JdexoqRN9tDxkIrYZq0SOF44= github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM= github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g= -github.com/aws/aws-sdk-go-v2/credentials v1.17.66 h1:aKpEKaTy6n4CEJeYI1MNj97oSDLi4xro3UzQfwf5RWE= -github.com/aws/aws-sdk-go-v2/credentials v1.17.66/go.mod h1:xQ5SusDmHb/fy55wU0QqTy0yNfLqxzec59YcsRZB+rI= github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM= github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.71 h1:s43gLuY+zGmtpx+KybfFP4IckopmTfDOPdlf/L++N5I= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.71/go.mod h1:KH6wWmY3O3c/jVAjHk0MGzVAFDxkOSt42Eoe4ZO4ge0= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.72 h1:PcKMOZfp+kNtJTw2HF2op6SjDvwPBYRvz0Y24PQLUR4= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.72/go.mod h1:vq7/m7dahFXcdzWVOvvjasDI9RcsD3RsTfHmDundJYg= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= @@ -105,20 +97,14 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91LiqT1nbvzDukyqAlCv89ZmwaHw/ZFlFZg= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA= -github.com/aws/aws-sdk-go-v2/service/kms v1.38.2 h1:945yEU8s1zYwy9s/2JzEJoHKvbAaZEkPqt8TOuO6r/g= -github.com/aws/aws-sdk-go-v2/service/kms v1.38.2/go.mod h1:cQn6tAF77Di6m4huxovNM7NVAozWTZLsDRp9t8Z/WYk= github.com/aws/aws-sdk-go-v2/service/kms v1.38.3 h1:RivOtUH3eEu6SWnUMFHKAW4MqDOzWn1vGQ3S38Y5QMg= github.com/aws/aws-sdk-go-v2/service/kms v1.38.3/go.mod h1:cQn6tAF77Di6m4huxovNM7NVAozWTZLsDRp9t8Z/WYk= -github.com/aws/aws-sdk-go-v2/service/s3 v1.79.1 h1:2Ku1xwAohSSXHR1tpAnyVDSQSxoDMA+/NZBytW+f4qg= -github.com/aws/aws-sdk-go-v2/service/s3 v1.79.1/go.mod h1:U5SNqwhXB3Xe6F47kXvWihPl/ilGaEDe8HD/50Z9wxc= github.com/aws/aws-sdk-go-v2/service/s3 v1.79.2 h1:tWUG+4wZqdMl/znThEk9tcCy8tTMxq8dW0JTgamohrY= github.com/aws/aws-sdk-go-v2/service/s3 v1.79.2/go.mod h1:U5SNqwhXB3Xe6F47kXvWihPl/ilGaEDe8HD/50Z9wxc= github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8= github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.18 h1:xz7WvTMfSStb9Y8NpCT82FXLNC3QasqBfuAFHY4Pk5g= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.18/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY= github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k= @@ -131,16 +117,14 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk= -github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f h1:C5bqEmzEPLsHm9Mv73lSE9e9bKV23aB1vxOsmZrkl3k= github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/compose-spec/compose-go v1.20.2 h1:u/yfZHn4EaHGdidrZycWpxXgFffjYULlTbRfJ51ykjQ= github.com/compose-spec/compose-go v1.20.2/go.mod h1:+MdqXV4RA7wdFsahh/Kb8U0pAJqkg7mr4PM9tFKU8RM= -github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= -github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= +github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= +github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -154,14 +138,16 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/cli v28.0.4+incompatible h1:pBJSJeNd9QeIWPjRcV91RVJihd/TXB77q1ef64XEu4A= -github.com/docker/cli v28.0.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v28.0.4+incompatible h1:JNNkBctYKurkw6FrHfKqY0nKIDf5nrbxjVBtS+cdcok= -github.com/docker/docker v28.0.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/cli v27.0.1+incompatible h1:d/OrlblkOTkhJ1IaAGD1bLgUBtFQC/oP0VjkFMIN+B0= +github.com/docker/cli v27.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v27.0.1+incompatible h1:AbszR+lCnR3f297p/g0arbQoyhAkImxQOR/XO9YZeIg= +github.com/docker/docker v27.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a h1:UwSIFv5g5lIvbGgtf3tVwC7Ky9rmMFBp0RMs+6f6YqE= +github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= @@ -172,6 +158,8 @@ github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/extism/go-sdk v1.7.0 h1:yHbSa2JbcF60kjGsYiGEOcClfbknqCJchyh9TRibFWo= +github.com/extism/go-sdk v1.7.0/go.mod h1:Dhuc1qcD0aqjdqJ3ZDyGdkZPEj/EHKVjbE4P+1XRMqc= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -190,8 +178,8 @@ github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vt github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/getsops/gopgagent v0.0.0-20241224165529-7044f28e491e h1:y/1nzrdF+RPds4lfoEpNhjfmzlgZtPqyO3jMzrqDQws= github.com/getsops/gopgagent v0.0.0-20241224165529-7044f28e491e/go.mod h1:awFzISqLJoZLm+i9QQ4SgMNHDqljH6jWV0B36V5MrUM= -github.com/getsops/sops/v3 v3.10.1 h1:5xh+9yTN2sa0iAKlBWyBqu8SEzRsrlbsjdidDoHwY90= -github.com/getsops/sops/v3 v3.10.1/go.mod h1:ywVxyFHOiHoJN9uXLO3aEj71OoRfaxlkOzR2wjduZLo= +github.com/getsops/sops/v3 v3.9.0 h1:J1UGOAPz4wSRE1dRtkwcQNyvG/jcjcRYJy1wbgKbqeE= +github.com/getsops/sops/v3 v3.9.0/go.mod h1:lYvaahx9fme8XdBLFHLAZzsMuApg8pIJn8ApyInTdqk= github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -212,8 +200,10 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= -github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc= +github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY= github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -273,6 +263,8 @@ github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3q github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= github.com/hashicorp/vault/api v1.16.0 h1:nbEYGJiAPGzT9U4oWgaaB0g+Rj8E59QuHKyA5LhwQN4= github.com/hashicorp/vault/api v1.16.0/go.mod h1:KhuUhzOD8lDSk29AtzNjgAu2kxRA9jL9NAbkFlqvkBA= +github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca h1:T54Ema1DU8ngI+aef9ZhAhNGQhcRTrWxVeG07F+c/Rw= +github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -307,10 +299,8 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= -github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= -github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= -github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -324,12 +314,12 @@ github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= -github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/opencontainers/runc v1.2.6 h1:P7Hqg40bsMvQGCS4S7DJYhUZOISMLJOB2iGX5COWiPk= -github.com/opencontainers/runc v1.2.6/go.mod h1:dOQeFo29xZKBNeRBI0B19mJtfHv68YgCTh1X+YphA+4= -github.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNGuyA= -github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/runc v1.1.13 h1:98S2srgG9vw0zWcDpFMn5TRrh8kLxa/5OFUstuUhmRs= +github.com/opencontainers/runc v1.1.13/go.mod h1:R016aXacfp/gwQBYw2FDGa9m+n6atbLWrYY8hNMT/sA= +github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4= +github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -369,6 +359,10 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q= +github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk= +github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4= +github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ= github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= @@ -407,13 +401,13 @@ go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5J go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= -golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= -golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -424,8 +418,6 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= @@ -456,8 +448,6 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -466,22 +456,10 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.228.0 h1:X2DJ/uoWGnY5obVjewbp8icSL5U4FzuCfy9OjbLSnLs= google.golang.org/api v0.228.0/go.mod h1:wNvRS1Pbe8r4+IfBIniV8fwCpGwTrYa+kMUDiC5z5a4= -google.golang.org/genproto v0.0.0-20250404141209-ee84b53bf3d0 h1:wX+y2uwLyC73sX9zfiJW7E7m68+oxAQGzgCmoM0e/zs= -google.golang.org/genproto v0.0.0-20250404141209-ee84b53bf3d0/go.mod h1:jwIveCnYVWLDIe0ZXnIrfMKNoy/rQRSRrepUPEruz0U= -google.golang.org/genproto v0.0.0-20250407143221-ac9807e6c755 h1:bldQzRMfyYSvYsP0oCgOIsBryyDN2Ci7HxB+rx3L7Qw= -google.golang.org/genproto v0.0.0-20250407143221-ac9807e6c755/go.mod h1:qD4k1RhYfNmRjqaHJxKLG/HRtqbXVclhjop2mPlxGwA= google.golang.org/genproto v0.0.0-20250409194420-de1ac958c67a h1:AoyioNVZR+nS6zbvnvW5rjQdeQu7/BWwIT7YI8Gq5wU= google.golang.org/genproto v0.0.0-20250409194420-de1ac958c67a/go.mod h1:qD4k1RhYfNmRjqaHJxKLG/HRtqbXVclhjop2mPlxGwA= -google.golang.org/genproto/googleapis/api v0.0.0-20250404141209-ee84b53bf3d0 h1:Qbb5RVn5xzI4naMJSpJ7lhvmos6UwZkbekd5Uz7rt9E= -google.golang.org/genproto/googleapis/api v0.0.0-20250404141209-ee84b53bf3d0/go.mod h1:6T35kB3IPpdw7Wul09by0G/JuOuIFkXV6OOvt8IZeT8= -google.golang.org/genproto/googleapis/api v0.0.0-20250407143221-ac9807e6c755 h1:AMLTAunltONNuzWgVPZXrjLWtXpsG6A3yLLPEoJ/IjU= -google.golang.org/genproto/googleapis/api v0.0.0-20250407143221-ac9807e6c755/go.mod h1:2R6XrVC8Oc08GlNh8ujEpc7HkLiEZ16QeY7FxIs20ac= google.golang.org/genproto/googleapis/api v0.0.0-20250409194420-de1ac958c67a h1:OQ7sHVzkx6L57dQpzUS4ckfWJ51KDH74XHTDe23xWAs= google.golang.org/genproto/googleapis/api v0.0.0-20250409194420-de1ac958c67a/go.mod h1:2R6XrVC8Oc08GlNh8ujEpc7HkLiEZ16QeY7FxIs20ac= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250404141209-ee84b53bf3d0 h1:0K7wTWyzxZ7J+L47+LbFogJW1nn/gnnMCN0vGXNYtTI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250404141209-ee84b53bf3d0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250407143221-ac9807e6c755 h1:TwXJCGVREgQ/cl18iY0Z4wJCTL/GmW+Um2oSwZiZPnc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250407143221-ac9807e6c755/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/rpc v0.0.0-20250409194420-de1ac958c67a h1:GIqLhp/cYUkuGuiT+vJk8vhOP86L4+SP5j8yXgeVpvI= google.golang.org/genproto/googleapis/rpc v0.0.0-20250409194420-de1ac958c67a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI= @@ -497,6 +475,7 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/pkg/config/config_handler_test.go b/pkg/config/config_handler_test.go index d06a6aafd..e87e47d53 100644 --- a/pkg/config/config_handler_test.go +++ b/pkg/config/config_handler_test.go @@ -42,7 +42,7 @@ func TestBaseConfigHandler_SetSecretsProvider(t *testing.T) { t.Run("Success", func(t *testing.T) { injector := di.NewInjector() handler := NewBaseConfigHandler(injector) - secretsProvider := secrets.NewMockSecretsProvider() + secretsProvider := secrets.NewMockSecretsProvider(injector) handler.SetSecretsProvider(secretsProvider) diff --git a/pkg/config/mock_config_handler_test.go b/pkg/config/mock_config_handler_test.go index 61ef1bd9b..f41a78343 100644 --- a/pkg/config/mock_config_handler_test.go +++ b/pkg/config/mock_config_handler_test.go @@ -103,7 +103,7 @@ func TestMockConfigHandler_SetSecretsProvider(t *testing.T) { t.Run("WithFuncSet", func(t *testing.T) { // Given a mock config handler with SetSecretsProviderFunc set handler := NewMockConfigHandler() - mockSecretsProvider := secrets.NewMockSecretsProvider() + mockSecretsProvider := secrets.NewMockSecretsProvider(di.NewMockInjector()) handler.SetSecretsProviderFunc = func(provider secrets.SecretsProvider) { if provider != mockSecretsProvider { t.Errorf("Expected provider = %v, got = %v", mockSecretsProvider, provider) @@ -117,7 +117,7 @@ func TestMockConfigHandler_SetSecretsProvider(t *testing.T) { t.Run("WithNoFuncSet", func(t *testing.T) { // Given a mock config handler without SetSecretsProviderFunc set handler := NewMockConfigHandler() - mockSecretsProvider := secrets.NewMockSecretsProvider() + mockSecretsProvider := secrets.NewMockSecretsProvider(di.NewMockInjector()) // When SetSecretsProvider is called handler.SetSecretsProvider(mockSecretsProvider) diff --git a/pkg/controller/controller_test.go b/pkg/controller/controller_test.go index 634d77044..92838a630 100644 --- a/pkg/controller/controller_test.go +++ b/pkg/controller/controller_test.go @@ -47,7 +47,7 @@ func setSafeControllerMocks(customInjector ...di.Injector) *MockObjects { // Create necessary mocks mockConfigHandler := config.NewMockConfigHandler() - mockSecretsProvider := secrets.NewMockSecretsProvider() + mockSecretsProvider := secrets.NewMockSecretsProvider(injector) mockEnvPrinter1 := env.NewMockEnvPrinter() mockEnvPrinter2 := env.NewMockEnvPrinter() // Use a mock instead of a real WindsorEnvPrinter @@ -451,7 +451,7 @@ func TestController_InitializeComponents(t *testing.T) { mocks := setSafeControllerMocks() controller := NewController(mocks.Injector) controller.Initialize() - mockSecretsProvider := secrets.NewMockSecretsProvider() + mockSecretsProvider := secrets.NewMockSecretsProvider(mocks.Injector) mockSecretsProvider.InitializeFunc = func() error { return fmt.Errorf("error initializing secrets provider") } diff --git a/pkg/controller/mock_controller.go b/pkg/controller/mock_controller.go index 41c1e5d34..218ff59f6 100644 --- a/pkg/controller/mock_controller.go +++ b/pkg/controller/mock_controller.go @@ -117,7 +117,7 @@ func (m *MockController) CreateSecretsProviders() error { } // Create a new mock secrets provider - secretsProvider := secrets.NewMockSecretsProvider() + secretsProvider := secrets.NewMockSecretsProvider(m.injector) m.injector.Register("secretsProvider", secretsProvider) return nil diff --git a/pkg/controller/real_controller.go b/pkg/controller/real_controller.go index ebe90ed83..cd34b1b1b 100644 --- a/pkg/controller/real_controller.go +++ b/pkg/controller/real_controller.go @@ -2,6 +2,7 @@ package controller import ( "fmt" + "os" "path/filepath" "strings" @@ -270,9 +271,18 @@ func (c *RealController) CreateSecretsProviders() error { vaults, ok := c.configHandler.Get(fmt.Sprintf("contexts.%s.secrets.onepassword.vaults", contextName)).(map[string]secretsConfigType.OnePasswordVault) if ok && len(vaults) > 0 { + useSDK := os.Getenv("OP_SERVICE_ACCOUNT_TOKEN") != "" + for key, vault := range vaults { vault.ID = key - opSecretsProvider := secrets.NewOnePasswordCLISecretsProvider(vault, c.injector) + var opSecretsProvider secrets.SecretsProvider + + if useSDK { + opSecretsProvider = secrets.NewOnePasswordSDKSecretsProvider(vault, c.injector) + } else { + opSecretsProvider = secrets.NewOnePasswordCLISecretsProvider(vault, c.injector) + } + c.injector.Register(fmt.Sprintf("op%sSecretsProvider", strings.ToUpper(key[:1])+key[1:]), opSecretsProvider) c.configHandler.SetSecretsProvider(opSecretsProvider) } diff --git a/pkg/env/windsor_env.go b/pkg/env/windsor_env.go index 9c6c7a6ad..efd9e64e1 100644 --- a/pkg/env/windsor_env.go +++ b/pkg/env/windsor_env.go @@ -59,12 +59,13 @@ func (e *WindsorEnvPrinter) Initialize() error { return nil } -// GetEnvVars constructs a map of Windsor-specific environment variables. It includes -// the current context, project root, and session token. Custom environment variables -// are also added, with secrets resolved using configured providers. Managed aliases -// and environment variables are appended to ensure a complete environment setup. -// This method ensures that all Windsor-prefixed variables and managed environment variables -// are included in the final environment setup, providing a comprehensive configuration. +// GetEnvVars constructs a map of Windsor-specific environment variables including +// the current context, project root, and session token. It resolves secrets in custom +// environment variables using configured providers, handles caching of values, and +// manages environment variables and aliases. For secrets, it leverages the secrets cache +// to avoid unnecessary decryption while ensuring variables are properly tracked in the +// managed environment list. Windsor-prefixed variables are automatically included in +// the final environment setup to provide a comprehensive configuration. func (e *WindsorEnvPrinter) GetEnvVars() (map[string]string, error) { envVars := make(map[string]string) @@ -85,13 +86,20 @@ func (e *WindsorEnvPrinter) GetEnvVars() (map[string]string, error) { originalEnvVars := e.configHandler.GetStringMap("environment") - // #nosec G101 # This is just a regular expression not a secret re := regexp.MustCompile(`\${{\s*(.*?)\s*}}`) + _, managedEnvExists := osLookupEnv("WINDSOR_MANAGED_ENV") + for k, v := range originalEnvVars { + if !managedEnvExists { + e.SetManagedEnv(k) + } + if re.MatchString(v) { if existingValue, exists := osLookupEnv(k); exists { - e.SetManagedEnv(k) + if managedEnvExists { + e.SetManagedEnv(k) + } if shouldUseCache() && !strings.Contains(existingValue, ".. }} patterns in the input // with corresponding secret values from 1Password, ensuring the id matches the vault ID. func (s *OnePasswordCLISecretsProvider) ParseSecrets(input string) (string, error) { - opPattern := `(?i)\${{\s*op(?:\.|\[)?\s*([^}]+)\s*}}` - re := regexp.MustCompile(opPattern) - - input = re.ReplaceAllStringFunc(input, func(match string) string { - submatches := re.FindStringSubmatch(match) - keyPath := strings.TrimSpace(submatches[1]) - - keys := ParseKeys(keyPath) - if len(keys) != 3 { - return "" - } + pattern := `(?i)\${{\s*op(?:\.|\[)?\s*([^}]+)\s*}}` + result := parseSecrets(input, pattern, func(keys []string) bool { + return len(keys) == 3 + }, func(keys []string) (string, bool) { id, secret, field := keys[0], keys[1], keys[2] - if id != s.vault.ID { - return match + return "", false } - value, err := s.GetSecret(fmt.Sprintf("%s.%s", secret, field)) if err != nil { - return "" + return "", true } - return value + return value, true }) - - return input, nil + return result, nil } // Ensure OnePasswordCLISecretsProvider implements SecretsProvider diff --git a/pkg/secrets/op_cli_secrets_provider_test.go b/pkg/secrets/op_cli_secrets_provider_test.go index 066488992..c7a32387c 100644 --- a/pkg/secrets/op_cli_secrets_provider_test.go +++ b/pkg/secrets/op_cli_secrets_provider_test.go @@ -1,360 +1,450 @@ package secrets import ( - "fmt" + "errors" "testing" secretsConfigType "github.com/windsorcli/cli/api/v1alpha1/secrets" - "github.com/windsorcli/cli/pkg/di" - "github.com/windsorcli/cli/pkg/shell" ) -type MockComponents struct { - Shell *shell.MockShell - Injector di.Injector -} +func TestNewOnePasswordCLISecretsProvider(t *testing.T) { + t.Run("Success", func(t *testing.T) { + // Setup mocks + mocks := setupSafeMocks() -func setupOnePasswordCLISecretsProviderMocks(injector ...di.Injector) *MockComponents { - var mockInjector di.Injector - if len(injector) > 0 { - mockInjector = injector[0] - } else { - mockInjector = di.NewInjector() - } + // Create a test vault + vault := secretsConfigType.OnePasswordVault{ + Name: "test-vault", + URL: "test-url", + } - // Create a new mock shell - mockShell := shell.NewMockShell() + // Create the provider + provider := NewOnePasswordCLISecretsProvider(vault, mocks.Injector) + + // Verify the provider was created correctly + if provider == nil { + t.Fatalf("Expected provider to be created, got nil") + } - mockInjector.Register("shell", mockShell) + // Verify the vault properties were set correctly + if provider.vault.Name != vault.Name { + t.Errorf("Expected vault name to be %s, got %s", vault.Name, provider.vault.Name) + } - return &MockComponents{ - Shell: mockShell, - Injector: mockInjector, - } + if provider.vault.URL != vault.URL { + t.Errorf("Expected vault URL to be %s, got %s", vault.URL, provider.vault.URL) + } + }) } func TestOnePasswordCLISecretsProvider_GetSecret(t *testing.T) { t.Run("Success", func(t *testing.T) { + // Setup mocks + mocks := setupSafeMocks() + + // Create a test vault vault := secretsConfigType.OnePasswordVault{ - URL: "https://example.1password.com", - Name: "ExampleVault", + Name: "test-vault", + URL: "test-url", } - // Setup mocks - mocks := setupOnePasswordCLISecretsProviderMocks() - execSilentCalled := false + // Create the provider + provider := NewOnePasswordCLISecretsProvider(vault, mocks.Injector) + + // Initialize the provider + err := provider.Initialize() + if err != nil { + t.Fatalf("Failed to initialize provider: %v", err) + } + + // Set the provider to unlocked state + provider.unlocked = true + + // Mock the shell.ExecSilent function to return a successful result mocks.Shell.ExecSilentFunc = func(command string, args ...string) (string, error) { - execSilentCalled = true - if command == "op" && - args[0] == "item" && - args[1] == "get" && - args[2] == "secretName" && - args[3] == "--vault" && - args[4] == "ExampleVault" && - args[5] == "--fields" && - args[6] == "fieldName" && - args[7] == "--reveal" && - args[8] == "--account" && - args[9] == "https://example.1password.com" { - return "secretValue", nil + // Verify the command and arguments + if command != "op" { + t.Errorf("Expected command to be 'op', got %s", command) + } + + // Check that the arguments contain the expected values + expectedArgs := []string{"item", "get", "test-secret", "--vault", "test-vault", "--fields", "password", "--reveal", "--account", "test-url"} + if len(args) != len(expectedArgs) { + t.Errorf("Expected %d arguments, got %d", len(expectedArgs), len(args)) } - return "", fmt.Errorf("unexpected command: %s", command) + + for i, arg := range args { + if i < len(expectedArgs) && arg != expectedArgs[i] { + t.Errorf("Expected argument %d to be %s, got %s", i, expectedArgs[i], arg) + } + } + + // Return a successful result + return "secret-value", nil + } + + // Call GetSecret + value, err := provider.GetSecret("test-secret.password") + + // Verify the result + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if value != "secret-value" { + t.Errorf("Expected value to be 'secret-value', got %s", value) } + }) + + t.Run("NotUnlocked", func(t *testing.T) { + // Setup mocks + mocks := setupSafeMocks() - // Pass the injector from mocks to the provider + // Create a test vault + vault := secretsConfigType.OnePasswordVault{ + Name: "test-vault", + URL: "test-url", + } + + // Create the provider provider := NewOnePasswordCLISecretsProvider(vault, mocks.Injector) // Initialize the provider err := provider.Initialize() if err != nil { - t.Fatalf("expected no error during initialization, got %v", err) + t.Fatalf("Failed to initialize provider: %v", err) } - // Set the provider as unlocked - provider.unlocked = true + // Set the provider to locked state + provider.unlocked = false - // Retrieve the secret - secret, err := provider.GetSecret("secretName.fieldName") - if err != nil { - t.Fatalf("expected no error, got %v", err) - } + // Call GetSecret + value, err := provider.GetSecret("test-secret.password") - if secret != "secretValue" { - t.Errorf("expected secretValue, got %s", secret) + // Verify the result + if err != nil { + t.Errorf("Expected no error, got %v", err) } - if !execSilentCalled { - t.Errorf("expected ExecSilent to be called, but it was not") + if value != "********" { + t.Errorf("Expected value to be '********', got %s", value) } }) t.Run("InvalidKeyFormat", func(t *testing.T) { + // Setup mocks + mocks := setupSafeMocks() + + // Create a test vault vault := secretsConfigType.OnePasswordVault{ - URL: "https://example.1password.com", - Name: "ExampleVault", + Name: "test-vault", + URL: "test-url", } - // Setup mocks - mocks := setupOnePasswordCLISecretsProviderMocks() + // Create the provider provider := NewOnePasswordCLISecretsProvider(vault, mocks.Injector) // Initialize the provider err := provider.Initialize() if err != nil { - t.Fatalf("expected no error during initialization, got %v", err) + t.Fatalf("Failed to initialize provider: %v", err) } - // Set the provider as unlocked + // Set the provider to unlocked state provider.unlocked = true - // Attempt to retrieve a secret with an invalid key format - _, err = provider.GetSecret("invalidKeyFormat") + // Call GetSecret with an invalid key format + value, err := provider.GetSecret("invalid-key") + + // Verify the result if err == nil { - t.Fatalf("expected an error due to invalid key format, got nil") + t.Error("Expected an error, got nil") + } + + expectedError := "invalid key notation: invalid-key. Expected format is 'secret.field'" + if err.Error() != expectedError { + t.Errorf("Expected error to be '%s', got '%s'", expectedError, err.Error()) } - expectedErrorMessage := "invalid key notation: invalidKeyFormat. Expected format is 'secret.field'" - if err.Error() != expectedErrorMessage { - t.Fatalf("expected error message to be %v, got %v", expectedErrorMessage, err.Error()) + if value != "" { + t.Errorf("Expected value to be empty, got %s", value) } }) - t.Run("RedactedSecretWhenLocked", func(t *testing.T) { + t.Run("CommandExecutionError", func(t *testing.T) { + // Setup mocks + mocks := setupSafeMocks() + + // Create a test vault vault := secretsConfigType.OnePasswordVault{ - URL: "https://example.1password.com", - Name: "ExampleVault", + Name: "test-vault", + URL: "test-url", } - // Setup mocks - mocks := setupOnePasswordCLISecretsProviderMocks() + // Create the provider provider := NewOnePasswordCLISecretsProvider(vault, mocks.Injector) // Initialize the provider err := provider.Initialize() if err != nil { - t.Fatalf("expected no error during initialization, got %v", err) + t.Fatalf("Failed to initialize provider: %v", err) } - // Ensure the provider is locked - provider.unlocked = false + // Set the provider to unlocked state + provider.unlocked = true - // Attempt to retrieve a secret while locked - secret, err := provider.GetSecret("secretName.fieldName") - if err != nil { - t.Fatalf("expected no error, got %v", err) + // Mock the shell.ExecSilent function to return an error + mocks.Shell.ExecSilentFunc = func(command string, args ...string) (string, error) { + return "", errors.New("command execution error") } - expectedRedactedSecret := "********" - if secret != expectedRedactedSecret { - t.Errorf("expected redacted secret %s, got %s", expectedRedactedSecret, secret) + // Call GetSecret + value, err := provider.GetSecret("test-secret.password") + + // Verify the result + if err == nil { + t.Error("Expected an error, got nil") + } + + expectedError := "failed to retrieve secret from 1Password: command execution error" + if err.Error() != expectedError { + t.Errorf("Expected error to be '%s', got '%s'", expectedError, err.Error()) + } + + if value != "" { + t.Errorf("Expected value to be empty, got %s", value) } }) } -func TestOnePasswordCLISecretsProvider_ParseSecrets(t *testing.T) { +func TestParseSecrets(t *testing.T) { t.Run("Success", func(t *testing.T) { - vault := secretsConfigType.OnePasswordVault{ - URL: "https://example.1password.com", - Name: "ExampleVault", - ID: "exampleVaultID", - } - // Setup mocks - mocks := setupOnePasswordCLISecretsProviderMocks() - execSilentCalled := false - mocks.Shell.ExecSilentFunc = func(command string, args ...string) (string, error) { - execSilentCalled = true - if command == "op" && args[0] == "item" && args[1] == "get" && args[2] == "secretName" && args[3] == "--vault" && args[4] == "ExampleVault" && args[5] == "--fields" && args[6] == "fieldName" { - return "secretValue", nil - } - return "", fmt.Errorf("unexpected command: %s", command) + mocks := setupSafeMocks() + + // Create a test vault + vault := secretsConfigType.OnePasswordVault{ + Name: "test-vault", + URL: "test-url", + ID: "test-id", } - // Pass the injector from mocks to the provider + // Create the provider provider := NewOnePasswordCLISecretsProvider(vault, mocks.Injector) - // Set the provider as unlocked - provider.unlocked = true - // Initialize the provider err := provider.Initialize() if err != nil { - t.Fatalf("expected no error during initialization, got %v", err) + t.Fatalf("Failed to initialize provider: %v", err) } - input := "The secret is ${{ op.exampleVaultID.secretName.fieldName }}." - expectedOutput := "The secret is secretValue." + // Set the provider to unlocked state + provider.unlocked = true + + // Mock the shell.ExecSilent function to return a successful result + mocks.Shell.ExecSilentFunc = func(command string, args ...string) (string, error) { + return "secret-value", nil + } + + // Test with standard notation + input := "This is a secret: ${{ op.test-id.test-secret.password }}" + expectedOutput := "This is a secret: secret-value" output, err := provider.ParseSecrets(input) + + // Verify the result if err != nil { - t.Fatalf("expected no error, got %v", err) + t.Errorf("Expected no error, got %v", err) } if output != expectedOutput { - t.Errorf("expected %s, got %s", expectedOutput, output) - } - - if !execSilentCalled { - t.Errorf("expected ExecSilent to be called, but it was not") + t.Errorf("Expected output to be '%s', got '%s'", expectedOutput, output) } }) - t.Run("MissingSecret", func(t *testing.T) { + t.Run("EmptyInput", func(t *testing.T) { + // Setup mocks + mocks := setupSafeMocks() + + // Create a test vault vault := secretsConfigType.OnePasswordVault{ - URL: "https://example.1password.com", - Name: "ExampleVault", - ID: "exampleVaultID", + Name: "test-vault", + URL: "test-url", + ID: "test-id", + } + + // Create the provider + provider := NewOnePasswordCLISecretsProvider(vault, mocks.Injector) + + // Initialize the provider + err := provider.Initialize() + if err != nil { + t.Fatalf("Failed to initialize provider: %v", err) } + // Test with empty input + input := "" + output, err := provider.ParseSecrets(input) + + // Verify the result + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if output != input { + t.Errorf("Expected output to be '%s', got '%s'", input, output) + } + }) + + t.Run("InvalidFormat", func(t *testing.T) { // Setup mocks - mocks := setupOnePasswordCLISecretsProviderMocks() - execSilentCalled := false - mocks.Shell.ExecSilentFunc = func(command string, args ...string) (string, error) { - execSilentCalled = true - return "", fmt.Errorf("item not found") + mocks := setupSafeMocks() + + // Create a test vault + vault := secretsConfigType.OnePasswordVault{ + Name: "test-vault", + URL: "test-url", + ID: "test-id", } - // Pass the injector from mocks to the provider + // Create the provider provider := NewOnePasswordCLISecretsProvider(vault, mocks.Injector) - // Set the provider as unlocked - provider.unlocked = true - // Initialize the provider err := provider.Initialize() if err != nil { - t.Fatalf("expected no error during initialization, got %v", err) + t.Fatalf("Failed to initialize provider: %v", err) } - input := "The secret is ${{ op.exampleVaultID.nonExistentSecret.fieldName }}" - expectedOutput := "The secret is " + // Test with invalid format (missing field) + input := "This is a secret: ${{ op.test-id.test-secret }}" + expectedOutput := "This is a secret: " output, err := provider.ParseSecrets(input) + + // Verify the result if err != nil { - t.Fatalf("expected no error, got %v", err) + t.Errorf("Expected no error, got %v", err) } if output != expectedOutput { - t.Errorf("expected %q, got %q", expectedOutput, output) - } - - if !execSilentCalled { - t.Errorf("expected ExecSilent to be called, but it was not") + t.Errorf("Expected output to be '%s', got '%s'", expectedOutput, output) } }) - t.Run("ReturnsErrorForInvalidKeyPath", func(t *testing.T) { + t.Run("MalformedJSON", func(t *testing.T) { + // Setup mocks + mocks := setupSafeMocks() + + // Create a test vault vault := secretsConfigType.OnePasswordVault{ - URL: "https://example.1password.com", - Name: "ExampleVault", - ID: "exampleVaultID", + Name: "test-vault", + URL: "test-url", + ID: "test-id", } - // Setup mocks - mocks := setupOnePasswordCLISecretsProviderMocks() + // Create the provider provider := NewOnePasswordCLISecretsProvider(vault, mocks.Injector) - // Set the provider as unlocked - provider.unlocked = true - // Initialize the provider err := provider.Initialize() if err != nil { - t.Fatalf("expected no error during initialization, got %v", err) + t.Fatalf("Failed to initialize provider: %v", err) } - // Test with invalid key path - input := "This is a secret: ${{ op.invalidFormat }}" - expectedOutput := "This is a secret: " + // Test with malformed JSON (missing closing brace) + input := "This is a secret: ${{ op.test-id.test-secret.password" + expectedOutput := "This is a secret: ${{ op.test-id.test-secret.password" output, err := provider.ParseSecrets(input) + + // Verify the result if err != nil { - t.Fatalf("expected no error, got %v", err) + t.Errorf("Expected no error, got %v", err) } if output != expectedOutput { - t.Errorf("expected %q, got %q", expectedOutput, output) + t.Errorf("Expected output to be '%s', got '%s'", expectedOutput, output) } }) - t.Run("DoesNotProcessMismatchedVaultID", func(t *testing.T) { + t.Run("MismatchedVaultID", func(t *testing.T) { + // Setup mocks + mocks := setupSafeMocks() + + // Create a test vault vault := secretsConfigType.OnePasswordVault{ - URL: "https://example.1password.com", - Name: "ExampleVault", - ID: "exampleVaultID", + Name: "test-vault", + URL: "test-url", + ID: "test-id", } - // Setup mocks - mocks := setupOnePasswordCLISecretsProviderMocks() + // Create the provider provider := NewOnePasswordCLISecretsProvider(vault, mocks.Injector) - // Set the provider as unlocked - provider.unlocked = true - // Initialize the provider err := provider.Initialize() if err != nil { - t.Fatalf("expected no error during initialization, got %v", err) + t.Fatalf("Failed to initialize provider: %v", err) } - // Test with a secret that should not be processed due to ID mismatch - input := "This is a secret: ${{ op.differentVaultID.secretName.fieldName }}" - expectedOutput := "This is a secret: ${{ op.differentVaultID.secretName.fieldName }}" + // Test with wrong vault ID + input := "This is a secret: ${{ op.wrong-id.test-secret.password }}" + expectedOutput := "This is a secret: ${{ op.wrong-id.test-secret.password }}" output, err := provider.ParseSecrets(input) + + // Verify the result if err != nil { - t.Fatalf("expected no error, got %v", err) + t.Errorf("Expected no error, got %v", err) } if output != expectedOutput { - t.Errorf("expected %q, got %q", expectedOutput, output) + t.Errorf("Expected output to be '%s', got '%s'", expectedOutput, output) } }) - t.Run("ReturnsErrorForEmptySecret", func(t *testing.T) { - vault := secretsConfigType.OnePasswordVault{ - URL: "https://example.1password.com", - Name: "ExampleVault", - ID: "exampleVaultID", - } - + t.Run("SecretNotFound", func(t *testing.T) { // Setup mocks - mocks := setupOnePasswordCLISecretsProviderMocks() - execSilentCalled := false - mocks.Shell.ExecSilentFunc = func(command string, args ...string) (string, error) { - execSilentCalled = true - if command == "op" && args[0] == "item" && args[1] == "get" && args[2] == "emptySecret" && args[3] == "--vault" && args[4] == "ExampleVault" && args[5] == "--fields" && args[6] == "fieldName" { - return "", nil - } - return "", fmt.Errorf("unexpected command: %s", command) + mocks := setupSafeMocks() + + // Create a test vault + vault := secretsConfigType.OnePasswordVault{ + Name: "test-vault", + URL: "test-url", + ID: "test-id", } - // Pass the injector from mocks to the provider + // Create the provider provider := NewOnePasswordCLISecretsProvider(vault, mocks.Injector) - // Set the provider as unlocked - provider.unlocked = true - // Initialize the provider err := provider.Initialize() if err != nil { - t.Fatalf("expected no error during initialization, got %v", err) + t.Fatalf("Failed to initialize provider: %v", err) } - input := "The secret is ${{ op.exampleVaultID.emptySecret.fieldName }}." - expectedOutput := "The secret is ." + // Set the provider to unlocked state + provider.unlocked = true + + // Mock the shell.ExecSilent function to return an error + mocks.Shell.ExecSilentFunc = func(command string, args ...string) (string, error) { + return "", errors.New("secret not found") + } + + // Test with a secret that doesn't exist + input := "This is a secret: ${{ op.test-id.nonexistent-secret.password }}" + expectedOutput := "This is a secret: " output, err := provider.ParseSecrets(input) + + // Verify the result if err != nil { - t.Fatalf("expected no error, got %v", err) + t.Errorf("Expected no error, got %v", err) } if output != expectedOutput { - t.Errorf("expected %q, got %q", expectedOutput, output) - } - - if !execSilentCalled { - t.Errorf("expected ExecSilent to be called, but it was not") + t.Errorf("Expected output to be '%s', got '%s'", expectedOutput, output) } }) } diff --git a/pkg/secrets/op_sdk_secrets_provider.go b/pkg/secrets/op_sdk_secrets_provider.go new file mode 100644 index 000000000..c853004e1 --- /dev/null +++ b/pkg/secrets/op_sdk_secrets_provider.go @@ -0,0 +1,124 @@ +package secrets + +import ( + "context" + "fmt" + "os" + "strings" + "sync" + + "github.com/1password/onepassword-sdk-go" + secretsConfigType "github.com/windsorcli/cli/api/v1alpha1/secrets" + "github.com/windsorcli/cli/pkg/di" +) + +var ( + globalClient *onepassword.Client + clientLock sync.Mutex +) + +// OnePasswordSDKSecretsProvider is an implementation of the SecretsProvider interface +// that uses the 1Password SDK to manage secrets. +type OnePasswordSDKSecretsProvider struct { + *BaseSecretsProvider + vault secretsConfigType.OnePasswordVault + client *onepassword.Client + ctx context.Context +} + +// NewOnePasswordSDKSecretsProvider creates a new OnePasswordSDKSecretsProvider instance +func NewOnePasswordSDKSecretsProvider(vault secretsConfigType.OnePasswordVault, injector di.Injector) *OnePasswordSDKSecretsProvider { + baseProvider := NewBaseSecretsProvider(injector) + return &OnePasswordSDKSecretsProvider{ + BaseSecretsProvider: baseProvider, + vault: vault, + ctx: context.Background(), + } +} + +// Initialize initializes the secrets provider +func (s *OnePasswordSDKSecretsProvider) Initialize() error { + if err := s.BaseSecretsProvider.Initialize(); err != nil { + return err + } + + // Get the service account token from environment + token := os.Getenv("OP_SERVICE_ACCOUNT_TOKEN") + if token == "" { + return fmt.Errorf("OP_SERVICE_ACCOUNT_TOKEN environment variable is required for 1Password SDK") + } + + return nil +} + +// GetSecret retrieves a secret value for the specified key +func (s *OnePasswordSDKSecretsProvider) GetSecret(key string) (string, error) { + if !s.isUnlocked() { + return "********", nil + } + + // Get the service account token from environment + token := os.Getenv("OP_SERVICE_ACCOUNT_TOKEN") + if token == "" { + return "", fmt.Errorf("OP_SERVICE_ACCOUNT_TOKEN environment variable is required for 1Password SDK") + } + + clientLock.Lock() + defer clientLock.Unlock() + + if globalClient == nil { + client, err := newOnePasswordClient( + s.ctx, + onepassword.WithServiceAccountToken(token), + onepassword.WithIntegrationInfo("windsor-cli", version), + ) + if err != nil { + return "", fmt.Errorf("failed to create 1Password client: %w", err) + } + if client == nil { + return "", fmt.Errorf("failed to create 1Password client: client is nil") + } + globalClient = client + s.client = globalClient + } + + parts := strings.SplitN(key, ".", 2) + if len(parts) != 2 { + return "", fmt.Errorf("invalid key notation: %s. Expected format is 'secret.field'", key) + } + + itemName := parts[0] + fieldName := parts[1] + + // Use secret reference URI format: op://vault/item/field + secretRef := fmt.Sprintf("op://%s/%s/%s", s.vault.ID, itemName, fieldName) + value, err := resolveSecret(s.client, s.ctx, secretRef) + if err != nil { + return "", fmt.Errorf("failed to resolve secret: %w", err) + } + + return value, nil +} + +// ParseSecrets identifies and replaces ${{ op... }} patterns in the input +// with corresponding secret values from 1Password, ensuring the id matches the vault ID. +func (s *OnePasswordSDKSecretsProvider) ParseSecrets(input string) (string, error) { + pattern := `(?i)\${{\s*op(?:\.|\[)?\s*([^}]+)\s*}}` + result := parseSecrets(input, pattern, func(keys []string) bool { + return len(keys) == 3 + }, func(keys []string) (string, bool) { + id, secret, field := keys[0], keys[1], keys[2] + if id != s.vault.ID { + return "", false + } + value, err := s.GetSecret(fmt.Sprintf("%s.%s", secret, field)) + if err != nil { + return fmt.Sprintf("", secret, field, err), true + } + return value, true + }) + return result, nil +} + +// Ensure OnePasswordSDKSecretsProvider implements SecretsProvider +var _ SecretsProvider = (*OnePasswordSDKSecretsProvider)(nil) diff --git a/pkg/secrets/op_sdk_secrets_provider_test.go b/pkg/secrets/op_sdk_secrets_provider_test.go new file mode 100644 index 000000000..f7661285e --- /dev/null +++ b/pkg/secrets/op_sdk_secrets_provider_test.go @@ -0,0 +1,652 @@ +package secrets + +import ( + "context" + "errors" + "os" + "testing" + + "github.com/1password/onepassword-sdk-go" + secretsConfigType "github.com/windsorcli/cli/api/v1alpha1/secrets" +) + +func TestNewOnePasswordSDKSecretsProvider(t *testing.T) { + t.Run("Success", func(t *testing.T) { + // Setup mocks + mocks := setupSafeMocks() + + // Create a test vault + vault := secretsConfigType.OnePasswordVault{ + Name: "test-vault", + ID: "test-id", + } + + // Create the provider + provider := NewOnePasswordSDKSecretsProvider(vault, mocks.Injector) + + // Verify the provider was created correctly + if provider == nil { + t.Fatalf("Expected provider to be created, got nil") + } + + // Verify the vault properties were set correctly + if provider.vault.Name != vault.Name { + t.Errorf("Expected vault name to be %s, got %s", vault.Name, provider.vault.Name) + } + + if provider.vault.ID != vault.ID { + t.Errorf("Expected vault ID to be %s, got %s", vault.ID, provider.vault.ID) + } + }) +} + +func TestOnePasswordSDKSecretsProvider_Initialize(t *testing.T) { + t.Run("Success", func(t *testing.T) { + // Setup mocks + mocks := setupSafeMocks() + + // Create a test vault + vault := secretsConfigType.OnePasswordVault{ + Name: "test-vault", + ID: "test-id", + } + + // Create the provider + provider := NewOnePasswordSDKSecretsProvider(vault, mocks.Injector) + + // Set environment variable + os.Setenv("OP_SERVICE_ACCOUNT_TOKEN", "test-token") + defer os.Unsetenv("OP_SERVICE_ACCOUNT_TOKEN") + + // Initialize the provider + err := provider.Initialize() + + // Verify the result + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) + + t.Run("MissingToken", func(t *testing.T) { + // Setup mocks + mocks := setupSafeMocks() + + // Create a test vault + vault := secretsConfigType.OnePasswordVault{ + Name: "test-vault", + ID: "test-id", + } + + // Create the provider + provider := NewOnePasswordSDKSecretsProvider(vault, mocks.Injector) + + // Ensure environment variable is not set + os.Unsetenv("OP_SERVICE_ACCOUNT_TOKEN") + + // Initialize the provider + err := provider.Initialize() + + // Verify the result + if err == nil { + t.Error("Expected an error, got nil") + } + + expectedError := "OP_SERVICE_ACCOUNT_TOKEN environment variable is required for 1Password SDK" + if err.Error() != expectedError { + t.Errorf("Expected error to be '%s', got '%s'", expectedError, err.Error()) + } + }) +} + +func TestOnePasswordSDKSecretsProvider_GetSecret(t *testing.T) { + t.Run("Success", func(t *testing.T) { + // Setup mocks + mocks := setupSafeMocks() + + // Create a test vault + vault := secretsConfigType.OnePasswordVault{ + Name: "test-vault", + ID: "test-id", + } + + // Create the provider + provider := NewOnePasswordSDKSecretsProvider(vault, mocks.Injector) + + // Set environment variable + os.Setenv("OP_SERVICE_ACCOUNT_TOKEN", "test-token") + defer os.Unsetenv("OP_SERVICE_ACCOUNT_TOKEN") + + // Set the provider to unlocked state + provider.unlocked = true + + // Override the shims for testing + originalNewClient := newOnePasswordClient + originalResolveSecret := resolveSecret + defer func() { + newOnePasswordClient = originalNewClient + resolveSecret = originalResolveSecret + }() + + // Set up the shims to use our mock + newOnePasswordClient = func(ctx context.Context, opts ...onepassword.ClientOption) (*onepassword.Client, error) { + return &onepassword.Client{}, nil + } + resolveSecret = func(client *onepassword.Client, ctx context.Context, secretRef string) (string, error) { + return "secret-value", nil + } + + // Call GetSecret + value, err := provider.GetSecret("test-secret.password") + + // Verify the result + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if value != "secret-value" { + t.Errorf("Expected value to be 'secret-value', got %s", value) + } + }) + + t.Run("NotUnlocked", func(t *testing.T) { + // Setup mocks + mocks := setupSafeMocks() + + // Create a test vault + vault := secretsConfigType.OnePasswordVault{ + Name: "test-vault", + ID: "test-id", + } + + // Create the provider + provider := NewOnePasswordSDKSecretsProvider(vault, mocks.Injector) + + // Set environment variable + os.Setenv("OP_SERVICE_ACCOUNT_TOKEN", "test-token") + defer os.Unsetenv("OP_SERVICE_ACCOUNT_TOKEN") + + // Set the provider to locked state + provider.unlocked = false + + // Call GetSecret + value, err := provider.GetSecret("test-secret.password") + + // Verify the result + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if value != "********" { + t.Errorf("Expected value to be '********', got %s", value) + } + }) + + t.Run("InvalidKeyFormat", func(t *testing.T) { + // Setup mocks + mocks := setupSafeMocks() + + // Create a test vault + vault := secretsConfigType.OnePasswordVault{ + Name: "test-vault", + ID: "test-id", + } + + // Create the provider + provider := NewOnePasswordSDKSecretsProvider(vault, mocks.Injector) + + // Set environment variable + os.Setenv("OP_SERVICE_ACCOUNT_TOKEN", "test-token") + defer os.Unsetenv("OP_SERVICE_ACCOUNT_TOKEN") + + // Set the provider to unlocked state + provider.unlocked = true + + // Call GetSecret with an invalid key format + value, err := provider.GetSecret("invalid-key") + + // Verify the result + if err == nil { + t.Error("Expected an error, got nil") + } + + expectedError := "invalid key notation: invalid-key. Expected format is 'secret.field'" + if err.Error() != expectedError { + t.Errorf("Expected error to be '%s', got '%s'", expectedError, err.Error()) + } + + if value != "" { + t.Errorf("Expected value to be empty, got %s", value) + } + }) + + t.Run("MissingToken", func(t *testing.T) { + // Setup mocks + mocks := setupSafeMocks() + + // Create a test vault + vault := secretsConfigType.OnePasswordVault{ + Name: "test-vault", + ID: "test-id", + } + + // Create the provider + provider := NewOnePasswordSDKSecretsProvider(vault, mocks.Injector) + + // Ensure environment variable is not set + os.Unsetenv("OP_SERVICE_ACCOUNT_TOKEN") + + // Set the provider to unlocked state + provider.unlocked = true + + // Call GetSecret + value, err := provider.GetSecret("test-secret.password") + + // Verify the result + if err == nil { + t.Error("Expected an error, got nil") + } + + expectedError := "OP_SERVICE_ACCOUNT_TOKEN environment variable is required for 1Password SDK" + if err.Error() != expectedError { + t.Errorf("Expected error to be '%s', got '%s'", expectedError, err.Error()) + } + + if value != "" { + t.Errorf("Expected value to be empty, got %s", value) + } + }) + + t.Run("ClientCreationError", func(t *testing.T) { + // Setup mocks + mocks := setupSafeMocks() + + // Create a test vault + vault := secretsConfigType.OnePasswordVault{ + Name: "test-vault", + ID: "test-id", + } + + // Create the provider + provider := NewOnePasswordSDKSecretsProvider(vault, mocks.Injector) + + // Set environment variable + os.Setenv("OP_SERVICE_ACCOUNT_TOKEN", "test-token") + defer os.Unsetenv("OP_SERVICE_ACCOUNT_TOKEN") + + // Set the provider to unlocked state + provider.unlocked = true + + // Reset global client + globalClient = nil + + // Override the shims for testing + originalNewClient := newOnePasswordClient + defer func() { + newOnePasswordClient = originalNewClient + }() + + // Set up the shims to use our mock + newOnePasswordClient = func(ctx context.Context, opts ...onepassword.ClientOption) (*onepassword.Client, error) { + return nil, errors.New("client creation error") + } + + // Call GetSecret + value, err := provider.GetSecret("test-secret.password") + + // Verify the result + if err == nil { + t.Error("Expected an error, got nil") + } + + expectedError := "failed to create 1Password client: client creation error" + if err.Error() != expectedError { + t.Errorf("Expected error to be '%s', got '%s'", expectedError, err.Error()) + } + + if value != "" { + t.Errorf("Expected value to be empty, got %s", value) + } + }) + + t.Run("SecretResolutionError", func(t *testing.T) { + // Setup mocks + mocks := setupSafeMocks() + + // Create a test vault + vault := secretsConfigType.OnePasswordVault{ + Name: "test-vault", + ID: "test-id", + } + + // Create the provider + provider := NewOnePasswordSDKSecretsProvider(vault, mocks.Injector) + + // Set environment variable + os.Setenv("OP_SERVICE_ACCOUNT_TOKEN", "test-token") + defer os.Unsetenv("OP_SERVICE_ACCOUNT_TOKEN") + + // Set the provider to unlocked state + provider.unlocked = true + + // Override the shims for testing + originalNewClient := newOnePasswordClient + originalResolveSecret := resolveSecret + defer func() { + newOnePasswordClient = originalNewClient + resolveSecret = originalResolveSecret + }() + + // Set up the shims to use our mock + newOnePasswordClient = func(ctx context.Context, opts ...onepassword.ClientOption) (*onepassword.Client, error) { + return &onepassword.Client{}, nil + } + resolveSecret = func(client *onepassword.Client, ctx context.Context, secretRef string) (string, error) { + return "", errors.New("secret resolution error") + } + + // Call GetSecret + value, err := provider.GetSecret("test-secret.password") + + // Verify the result + if err == nil { + t.Error("Expected an error, got nil") + } + + expectedError := "failed to resolve secret: secret resolution error" + if err.Error() != expectedError { + t.Errorf("Expected error to be '%s', got '%s'", expectedError, err.Error()) + } + + if value != "" { + t.Errorf("Expected value to be empty, got %s", value) + } + }) + + t.Run("NilClient", func(t *testing.T) { + // Setup mocks + mocks := setupSafeMocks() + + // Create a test vault + vault := secretsConfigType.OnePasswordVault{ + Name: "test-vault", + ID: "test-id", + } + + // Create the provider + provider := NewOnePasswordSDKSecretsProvider(vault, mocks.Injector) + + // Set environment variable + os.Setenv("OP_SERVICE_ACCOUNT_TOKEN", "test-token") + defer os.Unsetenv("OP_SERVICE_ACCOUNT_TOKEN") + + // Set the provider to unlocked state + provider.unlocked = true + + // Reset global client + globalClient = nil + + // Override the shims for testing + originalNewClient := newOnePasswordClient + defer func() { + newOnePasswordClient = originalNewClient + }() + + // Set up the shims to use our mock + newOnePasswordClient = func(ctx context.Context, opts ...onepassword.ClientOption) (*onepassword.Client, error) { + return nil, nil + } + + // Call GetSecret + value, err := provider.GetSecret("test-secret.password") + + // Verify the result + if err == nil { + t.Error("Expected an error, got nil") + } + + expectedError := "failed to create 1Password client: client is nil" + if err.Error() != expectedError { + t.Errorf("Expected error to be '%s', got '%s'", expectedError, err.Error()) + } + + if value != "" { + t.Errorf("Expected value to be empty, got %s", value) + } + }) +} + +func TestOnePasswordSDKSecretsProvider_ParseSecrets(t *testing.T) { + t.Run("Success", func(t *testing.T) { + // Setup mocks + mocks := setupSafeMocks() + + // Create a test vault + vault := secretsConfigType.OnePasswordVault{ + Name: "test-vault", + ID: "test-id", + } + + // Create the provider + provider := NewOnePasswordSDKSecretsProvider(vault, mocks.Injector) + + // Set environment variable + os.Setenv("OP_SERVICE_ACCOUNT_TOKEN", "test-token") + defer os.Unsetenv("OP_SERVICE_ACCOUNT_TOKEN") + + // Set the provider to unlocked state + provider.unlocked = true + + // Override the shims for testing + originalNewClient := newOnePasswordClient + originalResolveSecret := resolveSecret + defer func() { + newOnePasswordClient = originalNewClient + resolveSecret = originalResolveSecret + }() + + // Set up the shims to use our mock + newOnePasswordClient = func(ctx context.Context, opts ...onepassword.ClientOption) (*onepassword.Client, error) { + return &onepassword.Client{}, nil + } + resolveSecret = func(client *onepassword.Client, ctx context.Context, secretRef string) (string, error) { + return "secret-value", nil + } + + // Test with standard notation + input := "This is a secret: ${{ op.test-id.test-secret.password }}" + expectedOutput := "This is a secret: secret-value" + + output, err := provider.ParseSecrets(input) + + // Verify the result + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if output != expectedOutput { + t.Errorf("Expected output to be '%s', got '%s'", expectedOutput, output) + } + }) + + t.Run("EmptyInput", func(t *testing.T) { + // Setup mocks + mocks := setupSafeMocks() + + // Create a test vault + vault := secretsConfigType.OnePasswordVault{ + Name: "test-vault", + ID: "test-id", + } + + // Create the provider + provider := NewOnePasswordSDKSecretsProvider(vault, mocks.Injector) + + // Set environment variable + os.Setenv("OP_SERVICE_ACCOUNT_TOKEN", "test-token") + defer os.Unsetenv("OP_SERVICE_ACCOUNT_TOKEN") + + // Test with empty input + input := "" + output, err := provider.ParseSecrets(input) + + // Verify the result + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if output != input { + t.Errorf("Expected output to be '%s', got '%s'", input, output) + } + }) + + t.Run("InvalidFormat", func(t *testing.T) { + // Setup mocks + mocks := setupSafeMocks() + + // Create a test vault + vault := secretsConfigType.OnePasswordVault{ + Name: "test-vault", + ID: "test-id", + } + + // Create the provider + provider := NewOnePasswordSDKSecretsProvider(vault, mocks.Injector) + + // Set environment variable + os.Setenv("OP_SERVICE_ACCOUNT_TOKEN", "test-token") + defer os.Unsetenv("OP_SERVICE_ACCOUNT_TOKEN") + + // Test with invalid format (missing field) + input := "This is a secret: ${{ op.test-id.test-secret }}" + expectedOutput := "This is a secret: " + + output, err := provider.ParseSecrets(input) + + // Verify the result + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if output != expectedOutput { + t.Errorf("Expected output to be '%s', got '%s'", expectedOutput, output) + } + }) + + t.Run("MalformedJSON", func(t *testing.T) { + // Setup mocks + mocks := setupSafeMocks() + + // Create a test vault + vault := secretsConfigType.OnePasswordVault{ + Name: "test-vault", + ID: "test-id", + } + + // Create the provider + provider := NewOnePasswordSDKSecretsProvider(vault, mocks.Injector) + + // Set environment variable + os.Setenv("OP_SERVICE_ACCOUNT_TOKEN", "test-token") + defer os.Unsetenv("OP_SERVICE_ACCOUNT_TOKEN") + + // Test with malformed JSON (missing closing brace) + input := "This is a secret: ${{ op.test-id.test-secret.password" + expectedOutput := "This is a secret: ${{ op.test-id.test-secret.password" + + output, err := provider.ParseSecrets(input) + + // Verify the result + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if output != expectedOutput { + t.Errorf("Expected output to be '%s', got '%s'", expectedOutput, output) + } + }) + + t.Run("MismatchedVaultID", func(t *testing.T) { + // Setup mocks + mocks := setupSafeMocks() + + // Create a test vault + vault := secretsConfigType.OnePasswordVault{ + Name: "test-vault", + ID: "test-id", + } + + // Create the provider + provider := NewOnePasswordSDKSecretsProvider(vault, mocks.Injector) + + // Set environment variable + os.Setenv("OP_SERVICE_ACCOUNT_TOKEN", "test-token") + defer os.Unsetenv("OP_SERVICE_ACCOUNT_TOKEN") + + // Test with wrong vault ID + input := "This is a secret: ${{ op.wrong-id.test-secret.password }}" + expectedOutput := "This is a secret: ${{ op.wrong-id.test-secret.password }}" + + output, err := provider.ParseSecrets(input) + + // Verify the result + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if output != expectedOutput { + t.Errorf("Expected output to be '%s', got '%s'", expectedOutput, output) + } + }) + + t.Run("SecretNotFound", func(t *testing.T) { + // Setup mocks + mocks := setupSafeMocks() + + // Create a test vault + vault := secretsConfigType.OnePasswordVault{ + Name: "test-vault", + ID: "test-id", + } + + // Create the provider + provider := NewOnePasswordSDKSecretsProvider(vault, mocks.Injector) + + // Set environment variable + os.Setenv("OP_SERVICE_ACCOUNT_TOKEN", "test-token") + defer os.Unsetenv("OP_SERVICE_ACCOUNT_TOKEN") + + // Set the provider to unlocked state + provider.unlocked = true + + // Override the shims for testing + originalNewClient := newOnePasswordClient + originalResolveSecret := resolveSecret + defer func() { + newOnePasswordClient = originalNewClient + resolveSecret = originalResolveSecret + }() + + // Set up the shims to use our mock + newOnePasswordClient = func(ctx context.Context, opts ...onepassword.ClientOption) (*onepassword.Client, error) { + return &onepassword.Client{}, nil + } + resolveSecret = func(client *onepassword.Client, ctx context.Context, secretRef string) (string, error) { + return "", errors.New("secret not found") + } + + // Test with a secret that doesn't exist + input := "This is a secret: ${{ op.test-id.nonexistent-secret.password }}" + expectedOutput := "This is a secret: " + + output, err := provider.ParseSecrets(input) + + // Verify the result + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if output != expectedOutput { + t.Errorf("Expected output to be '%s', got '%s'", expectedOutput, output) + } + }) +} diff --git a/pkg/secrets/secrets_provider.go b/pkg/secrets/secrets_provider.go index 380e2384c..b890185c7 100644 --- a/pkg/secrets/secrets_provider.go +++ b/pkg/secrets/secrets_provider.go @@ -2,12 +2,15 @@ package secrets import ( "fmt" + "regexp" "strings" "github.com/windsorcli/cli/pkg/di" "github.com/windsorcli/cli/pkg/shell" ) +var version = "dev" + // SecretsProvider defines the interface for handling secrets operations type SecretsProvider interface { // Initialize initializes the secrets provider @@ -21,10 +24,14 @@ type SecretsProvider interface { // ParseSecrets parses a string and replaces ${{ secrets. }} references with their values ParseSecrets(input string) (string, error) + + // isUnlocked returns true if the secrets provider is locked (not unlocked) + isUnlocked() bool } // BaseSecretsProvider is a base implementation of the SecretsProvider interface type BaseSecretsProvider struct { + SecretsProvider secrets map[string]string unlocked bool shell shell.Shell @@ -68,20 +75,56 @@ func (s *BaseSecretsProvider) LoadSecrets() error { // GetSecret retrieves a secret value for the specified key func (s *BaseSecretsProvider) GetSecret(key string) (string, error) { - // Placeholder logic for retrieving a secret - return "", nil + panic("GetSecret must be implemented by concrete provider") } // ParseSecrets is a placeholder function for parsing secrets func (s *BaseSecretsProvider) ParseSecrets(input string) (string, error) { - // Placeholder logic for parsing secrets - return input, nil + panic("ParseSecrets must be implemented by concrete provider") +} + +// isUnlocked returns true if the secrets provider is unlocked +func (s *BaseSecretsProvider) isUnlocked() bool { + return s.unlocked +} + +// Ensure BaseSecretsProvider implements SecretsProvider +var _ SecretsProvider = (*BaseSecretsProvider)(nil) + +// parseSecrets is a helper function that parses a string and replaces secret references with their values. +// It takes a pattern to match secret references, a function to validate the keys, and a function to get the secret value. +func parseSecrets(input string, pattern string, validateKeys func([]string) bool, getSecretValue func([]string) (string, bool)) string { + re := regexp.MustCompile(pattern) + + return re.ReplaceAllStringFunc(input, func(match string) string { + // Extract the key path from the match + submatches := re.FindStringSubmatch(match) + if len(submatches) < 2 || submatches[1] == "" { + return "" + } + keyPath := strings.TrimSpace(submatches[1]) + + // Parse the key path using parseKeys + keys := parseKeys(keyPath) + + // Validate the keys + if !validateKeys(keys) { + return fmt.Sprintf("", keyPath) + } + + // Get the secret value + value, shouldReplace := getSecretValue(keys) + if !shouldReplace { + return match + } + return value + }) } // ParseKeys processes a string path that may contain mixed dot and bracket notations, // extracting and returning an array of keys. It handles quoted strings within brackets // and treats consecutive dots as empty keys unless they follow a closing bracket. -func ParseKeys(path string) []string { +func parseKeys(path string) []string { var keys []string var currentKey strings.Builder var bracketDepth int @@ -174,6 +217,3 @@ func ParseKeys(path string) []string { return keys } - -// Ensure BaseSecretsProvider implements SecretsProvider -var _ SecretsProvider = (*BaseSecretsProvider)(nil) diff --git a/pkg/secrets/secrets_provider_test.go b/pkg/secrets/secrets_provider_test.go index beca6834c..7f617a0d2 100644 --- a/pkg/secrets/secrets_provider_test.go +++ b/pkg/secrets/secrets_provider_test.go @@ -58,10 +58,19 @@ func TestBaseSecretsProvider_LoadSecrets(t *testing.T) { }) } +type mockSecretsProvider struct { + *BaseSecretsProvider +} + +func (m *mockSecretsProvider) GetSecret(key string) (string, error) { + return "", nil +} + func TestBaseSecretsProvider_GetSecret(t *testing.T) { t.Run("SecretNotFound", func(t *testing.T) { mocks := setupSafeMocks() - provider := NewBaseSecretsProvider(mocks.Injector) + baseProvider := NewBaseSecretsProvider(mocks.Injector) + provider := &mockSecretsProvider{BaseSecretsProvider: baseProvider} value, err := provider.GetSecret("non_existent_key") if err != nil { @@ -71,22 +80,37 @@ func TestBaseSecretsProvider_GetSecret(t *testing.T) { t.Errorf("Expected GetSecret to return an empty string for non-existent key, but got: %s", value) } }) + + t.Run("PanicsWhenNotImplemented", func(t *testing.T) { + mocks := setupSafeMocks() + provider := NewBaseSecretsProvider(mocks.Injector) + + defer func() { + if r := recover(); r == nil { + t.Error("Expected GetSecret to panic, but it did not") + } else if r != "GetSecret must be implemented by concrete provider" { + t.Errorf("Expected panic message 'GetSecret must be implemented by concrete provider', got '%v'", r) + } + }() + + provider.GetSecret("test") + }) } func TestBaseSecretsProvider_ParseSecrets(t *testing.T) { - t.Run("NoSecretsToParse", func(t *testing.T) { + t.Run("PanicsWhenNotImplemented", func(t *testing.T) { mocks := setupSafeMocks() provider := NewBaseSecretsProvider(mocks.Injector) - input := "This is a test string with no secrets." - expectedOutput := "This is a test string with no secrets." - output, err := provider.ParseSecrets(input) - if err != nil { - t.Errorf("Expected ParseSecrets to not return an error, but got: %v", err) - } - if output != expectedOutput { - t.Errorf("Expected ParseSecrets to return '%s', but got: '%s'", expectedOutput, output) - } + defer func() { + if r := recover(); r == nil { + t.Error("Expected ParseSecrets to panic, but it did not") + } else if r != "ParseSecrets must be implemented by concrete provider" { + t.Errorf("Expected panic message 'ParseSecrets must be implemented by concrete provider', got '%v'", r) + } + }() + + provider.ParseSecrets("test") }) } @@ -175,7 +199,7 @@ func TestParseKeys(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := ParseKeys(tt.input) + result := parseKeys(tt.input) if result == nil { t.Errorf("ParseKeys returned nil for input %s", tt.input) return diff --git a/pkg/secrets/shims.go b/pkg/secrets/shims.go index d2b984e76..747b367df 100644 --- a/pkg/secrets/shims.go +++ b/pkg/secrets/shims.go @@ -1,8 +1,11 @@ package secrets import ( + "context" + "errors" "os" + "github.com/1password/onepassword-sdk-go" "github.com/getsops/sops/v3/decrypt" "github.com/goccy/go-yaml" ) @@ -15,3 +18,16 @@ var yamlUnmarshal = yaml.Unmarshal // decryptFileFunc is a shim for decrypt.File to allow for easier testing and mocking. var decryptFileFunc = decrypt.File + +// newOnePasswordClient is a shim for onepassword.NewClient to allow for easier testing and mocking. +var newOnePasswordClient = func(ctx context.Context, opts ...onepassword.ClientOption) (*onepassword.Client, error) { + return onepassword.NewClient(ctx, opts...) +} + +// resolveSecret is a shim for client.Secrets().Resolve to allow for easier testing and mocking. +var resolveSecret = func(client *onepassword.Client, ctx context.Context, secretRef string) (string, error) { + if client == nil { + return "", errors.New("client is nil") + } + return client.Secrets().Resolve(ctx, secretRef) +} diff --git a/pkg/secrets/sops_secrets_provider.go b/pkg/secrets/sops_secrets_provider.go index 0d9d23bba..43d97b51a 100644 --- a/pkg/secrets/sops_secrets_provider.go +++ b/pkg/secrets/sops_secrets_provider.go @@ -4,7 +4,6 @@ import ( "fmt" "os" "path/filepath" - "regexp" "strings" "github.com/windsorcli/cli/pkg/di" @@ -95,7 +94,7 @@ func (s *SopsSecretsProvider) LoadSecrets() error { // GetSecret retrieves a secret value for the specified key func (s *SopsSecretsProvider) GetSecret(key string) (string, error) { - if !s.unlocked { + if !s.isUnlocked() { return "********", nil } if value, ok := s.secrets[key]; ok { @@ -106,33 +105,22 @@ func (s *SopsSecretsProvider) GetSecret(key string) (string, error) { // ParseSecrets parses a string and replaces ${{ sops. }} references with their values func (s *SopsSecretsProvider) ParseSecrets(input string) (string, error) { - re := regexp.MustCompile(sopsPattern) - - input = re.ReplaceAllStringFunc(input, func(match string) string { - // Extract the key path from the match - submatches := re.FindStringSubmatch(match) - if len(submatches) < 2 || submatches[1] == "" { - return "" - } - keyPath := submatches[1] - - // Parse the key path using ParseKeys - keys := ParseKeys(keyPath) + result := parseSecrets(input, sopsPattern, func(keys []string) bool { for _, key := range keys { if key == "" { - return fmt.Sprintf("", keyPath) + return false } } - - // Retrieve the secret value - value, err := s.GetSecret(strings.Join(keys, ".")) + return true + }, func(keys []string) (string, bool) { + key := strings.Join(keys, ".") + value, err := s.GetSecret(key) if err != nil { - return "" + return fmt.Sprintf("", key), true } - return value + return value, true }) - - return input, nil + return result, nil } // Ensure SopsSecretsProvider implements the SecretsProvider interface From 43d4d37d113c02f489e271988ec4da59f27bb508 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy Date: Sat, 12 Apr 2025 07:23:00 -0400 Subject: [PATCH 2/2] Fix 1Password multi-vault malfunction (#796) * Remove breaks so providers are created * go deps * Only parse secrets belonging to the provider * Improve regex * Add test tidy --- pkg/secrets/op_sdk_secrets_provider.go | 25 ++++++++++++--------- pkg/secrets/op_sdk_secrets_provider_test.go | 12 ++++++++++ 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/pkg/secrets/op_sdk_secrets_provider.go b/pkg/secrets/op_sdk_secrets_provider.go index c853004e1..53f0d35f6 100644 --- a/pkg/secrets/op_sdk_secrets_provider.go +++ b/pkg/secrets/op_sdk_secrets_provider.go @@ -14,6 +14,7 @@ import ( var ( globalClient *onepassword.Client + globalCtx context.Context clientLock sync.Mutex ) @@ -21,9 +22,7 @@ var ( // that uses the 1Password SDK to manage secrets. type OnePasswordSDKSecretsProvider struct { *BaseSecretsProvider - vault secretsConfigType.OnePasswordVault - client *onepassword.Client - ctx context.Context + vault secretsConfigType.OnePasswordVault } // NewOnePasswordSDKSecretsProvider creates a new OnePasswordSDKSecretsProvider instance @@ -32,7 +31,6 @@ func NewOnePasswordSDKSecretsProvider(vault secretsConfigType.OnePasswordVault, return &OnePasswordSDKSecretsProvider{ BaseSecretsProvider: baseProvider, vault: vault, - ctx: context.Background(), } } @@ -51,13 +49,16 @@ func (s *OnePasswordSDKSecretsProvider) Initialize() error { return nil } -// GetSecret retrieves a secret value for the specified key +// GetSecret retrieves a secret value for the specified key. It first checks if the provider is unlocked. +// If not, it returns a masked value. It then ensures the 1Password client is initialized using a +// service account token from the environment. The key is split into item and field parts, and the +// item name is sanitized. A secret reference URI is constructed and used to resolve the secret value +// from 1Password. If successful, the secret value is returned; otherwise, an error is reported. func (s *OnePasswordSDKSecretsProvider) GetSecret(key string) (string, error) { if !s.isUnlocked() { return "********", nil } - // Get the service account token from environment token := os.Getenv("OP_SERVICE_ACCOUNT_TOKEN") if token == "" { return "", fmt.Errorf("OP_SERVICE_ACCOUNT_TOKEN environment variable is required for 1Password SDK") @@ -67,8 +68,9 @@ func (s *OnePasswordSDKSecretsProvider) GetSecret(key string) (string, error) { defer clientLock.Unlock() if globalClient == nil { + globalCtx = context.Background() client, err := newOnePasswordClient( - s.ctx, + globalCtx, onepassword.WithServiceAccountToken(token), onepassword.WithIntegrationInfo("windsor-cli", version), ) @@ -79,7 +81,6 @@ func (s *OnePasswordSDKSecretsProvider) GetSecret(key string) (string, error) { return "", fmt.Errorf("failed to create 1Password client: client is nil") } globalClient = client - s.client = globalClient } parts := strings.SplitN(key, ".", 2) @@ -90,9 +91,11 @@ func (s *OnePasswordSDKSecretsProvider) GetSecret(key string) (string, error) { itemName := parts[0] fieldName := parts[1] - // Use secret reference URI format: op://vault/item/field - secretRef := fmt.Sprintf("op://%s/%s/%s", s.vault.ID, itemName, fieldName) - value, err := resolveSecret(s.client, s.ctx, secretRef) + // Construct the secret reference URI + secretRef := fmt.Sprintf("op://%s/%s/%s", s.vault.Name, itemName, fieldName) + + // Resolve the secret using the SDK + value, err := resolveSecret(globalClient, globalCtx, secretRef) if err != nil { return "", fmt.Errorf("failed to resolve secret: %w", err) } diff --git a/pkg/secrets/op_sdk_secrets_provider_test.go b/pkg/secrets/op_sdk_secrets_provider_test.go index f7661285e..95f50f546 100644 --- a/pkg/secrets/op_sdk_secrets_provider_test.go +++ b/pkg/secrets/op_sdk_secrets_provider_test.go @@ -135,6 +135,18 @@ func TestOnePasswordSDKSecretsProvider_GetSecret(t *testing.T) { return "secret-value", nil } + // Initialize the global client + client, err := newOnePasswordClient(context.Background(), onepassword.WithServiceAccountToken("test-token")) + if err != nil { + t.Fatalf("Failed to create client: %v", err) + } + globalClient = client + + // Initialize the provider + if err := provider.Initialize(); err != nil { + t.Fatalf("Failed to initialize provider: %v", err) + } + // Call GetSecret value, err := provider.GetSecret("test-secret.password")