From 3793bdc2c30193dc2272238f253bc528255a72cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Laszlo?= Date: Tue, 19 Nov 2024 22:59:19 +0100 Subject: [PATCH 1/9] Add GCP Secret Manager adapter --- lib/kamal/secrets/adapters.rb | 1 + .../secrets/adapters/gcp_secret_manager.rb | 125 +++++++++++ .../gcp_secret_manager_adapter_test.rb | 211 ++++++++++++++++++ 3 files changed, 337 insertions(+) create mode 100644 lib/kamal/secrets/adapters/gcp_secret_manager.rb create mode 100644 test/secrets/gcp_secret_manager_adapter_test.rb diff --git a/lib/kamal/secrets/adapters.rb b/lib/kamal/secrets/adapters.rb index 439c7208..2abf29ad 100644 --- a/lib/kamal/secrets/adapters.rb +++ b/lib/kamal/secrets/adapters.rb @@ -3,6 +3,7 @@ module Kamal::Secrets::Adapters def self.lookup(name) name = "one_password" if name.downcase == "1password" name = "last_pass" if name.downcase == "lastpass" + name = "gcp_secret_manager" if %w[gcp secret_manager].include? name.downcase adapter_class(name) end diff --git a/lib/kamal/secrets/adapters/gcp_secret_manager.rb b/lib/kamal/secrets/adapters/gcp_secret_manager.rb new file mode 100644 index 00000000..c6d6387b --- /dev/null +++ b/lib/kamal/secrets/adapters/gcp_secret_manager.rb @@ -0,0 +1,125 @@ +class Kamal::Secrets::Adapters::GcpSecretManager < Kamal::Secrets::Adapters::Base + private + def login(account) + # Since only the account option is passed from the cli, we'll use it for: + # - Account + # - GCP project + # - Service account impersonation + # + # Syntax: + # ACCOUNT: USER | USER "," DELEGATION_CHAIN + # USER: DEFAULT_USER | EMAIL + # DELEGATION_CHAIN: EMAIL | EMAIL "," DELEGATION_CHAIN + # EMAIL: + # DEFAULT_USER: "default" + # + # Some valid examples: + # - "my-user@example.com" sets the user + # - "my-user@example.com,my-service-user@example.com" will use my-user and enable service account impersonation as my-service-user + # - "default" will use the default user and no impersonation + # - "default,my-service-user@example.com" will use the default user, and enable service account impersonation as my-service-user + # - "default,my-service-user@example.com,another-service-user@example.com" same as above, but with an impersonation delegation chain + + if !logged_in? + raise RuntimeError, "gcloud is not authenticated, please run `gcloud auth login`" + end + + user, impersonate_service_account = parse_account(account) + + { + user: user, + impersonate_service_account: impersonate_service_account + } + end + + def fetch_secrets(secrets, account:, session:) + # puts("secrets spec: #{secrets.inspect}") + {}.tap do |results| + secrets_with_metadata(secrets).each do |secret, metadata| + project, secret_name, secret_version = metadata + item_name = project == "default" ? secret_name : "#{project}/#{secret_name}" + results[item_name] = fetch_secret(session, project, secret_name, secret_version) + raise RuntimeError, "Could not read #{item_name} from Google Secret Manager" unless $?.success? + end + end + end + + def fetch_secret(session, project, secret_name, secret_version) + secret = run_command("secrets versions access #{secret_version} --secret=#{secret_name.shellescape}", session: session, project: project) + Base64.decode64(secret.dig("payload", "data")) + end + + # The secret needs to at least contain a secret name, but project name, and secret version can also be specified. + # + # The string "default" can be used to refer to the default project configured for gcloud. + # + # The version can be either the string "latest", or a version number. + # + # The following formats are valid: + # + # - The following are all equivalent, and sets project: default, secret name: my-secret, version: latest + # - "my-secret" + # - "default/my-secret" + # - "default/my-secret/latest" + # - "my-secret/latest" in combination with --from=default + # - "my-secret/123" (only in combination with --from=some-project) -> project: some-project, secret name: my-secret, version: 123 + # - "some-project/my-secret/123" -> project: some-project, secret name: my-secret, version: 123 + def secrets_with_metadata(secrets) + {}.tap do |items| + secrets.each do |secret| + parts = secret.split("/") + parts.unshift("default") if parts.length == 1 + project = parts.shift + secret_name = parts.shift + secret_version = parts.shift || "latest" + + items[secret] = [ project, secret_name, secret_version ] + end + end + end + + def run_command(command, session: nil, project: nil) + full_command = [ "gcloud", command ] + full_command << "--project=#{project}" unless project == "default" + full_command << "--account=#{session[:user]}" unless session[:user] == "default" + full_command << "--impersonate-service-account=#{session[:impersonate_service_account]}" if session[:impersonate_service_account] + full_command << "--format=json" + full_command = full_command.join(" ") + + result = `#{full_command}`.strip + JSON.parse(result) + end + + def check_dependencies! + raise RuntimeError, "gcloud CLI is not installed" unless cli_installed? + end + + def cli_installed? + `gcloud --version 2> /dev/null` + $?.success? + end + + def logged_in? + JSON.parse(`gcloud auth list --format=json`).any? + end + + def parse_account(account) + return "default", nil if account == "default" + + parts = account.split(",", 2) + + if parts.length == 2 + return parts.shift, parts.shift + elsif parts.length != 1 + raise RuntimeError, "Invalid account, too many parts: #{account}" + elsif is_user?(account) + return account, nil + end + + raise RuntimeError, "Invalid account, not a user: #{account}" + end + + def is_user?(candidate) + candidate.include?("@") + end +end diff --git a/test/secrets/gcp_secret_manager_adapter_test.rb b/test/secrets/gcp_secret_manager_adapter_test.rb new file mode 100644 index 00000000..3369d08e --- /dev/null +++ b/test/secrets/gcp_secret_manager_adapter_test.rb @@ -0,0 +1,211 @@ +require "test_helper" + +class GcpSecretManagerAdapterTest < SecretAdapterTestCase + test "fetch" do + stub_gcloud_version + stub_authenticated + stub_mypassword + + json = JSON.parse(shellunescape(run_command("fetch", "mypassword"))) + + expected_json = { "mypassword"=>"secret123" } + + assert_equal expected_json, json + end + + test "fetch unauthenticated" do + stub_ticks.with("gcloud --version 2> /dev/null") + + stub_mypassword + stub_unauthenticated + + error = assert_raises RuntimeError do + JSON.parse(shellunescape(run_command("fetch", "mypassword"))) + end + + assert_match(/not authenticated/, error.message) + end + + test "fetch with from" do + stub_gcloud_version + stub_authenticated + stub_items(0, project: "other-project") + stub_items(1, project: "other-project") + stub_items(2, project: "other-project") + + json = JSON.parse(shellunescape(run_command("fetch", "--from", "other-project", "item1", "item2", "item3"))) + + expected_json = { + "other-project/item1"=>"secret1", "other-project/item2"=>"secret2", "other-project/item3"=>"secret3" + } + + assert_equal expected_json, json + end + + test "fetch with multiple projects" do + stub_gcloud_version + stub_authenticated + stub_items(0, project: "some-project") + stub_items(1, project: "project-confidence") + stub_items(2, project: "manhattan-project") + + json = JSON.parse(shellunescape(run_command("fetch", "some-project/item1", "project-confidence/item2", "manhattan-project/item3"))) + + expected_json = { + "some-project/item1"=>"secret1", "project-confidence/item2"=>"secret2", "manhattan-project/item3"=>"secret3" + } + + assert_equal expected_json, json + end + + test "fetch with specific version" do + stub_gcloud_version + stub_authenticated + stub_items(0, project: "some-project", version: "123") + + json = JSON.parse(shellunescape(run_command("fetch", "some-project/item1/123"))) + + expected_json = { + "some-project/item1"=>"secret1" + } + + assert_equal expected_json, json + end + + test "fetch with non-default account" do + stub_gcloud_version + stub_authenticated + stub_items(0, project: "some-project", version: "123", account: "email@example.com") + + json = JSON.parse(shellunescape(run_command("fetch", "some-project/item1/123", account: "email@example.com"))) + + expected_json = { + "some-project/item1"=>"secret1" + } + + assert_equal expected_json, json + end + + test "fetch with service account impersonation" do + stub_gcloud_version + stub_authenticated + stub_items(0, project: "some-project", version: "123", impersonate_service_account: "service-user@example.com") + + json = JSON.parse(shellunescape(run_command("fetch", "some-project/item1/123", account: "default,service-user@example.com"))) + + expected_json = { + "some-project/item1"=>"secret1" + } + + assert_equal expected_json, json + end + + test "fetch with delegation chain and specific user" do + stub_gcloud_version + stub_authenticated + stub_items(0, project: "some-project", version: "123", account: "user@example.com", impersonate_service_account: "service-user@example.com,service-user2@example.com") + + json = JSON.parse(shellunescape(run_command("fetch", "some-project/item1/123", account: "user@example.com,service-user@example.com,service-user2@example.com"))) + + expected_json = { + "some-project/item1"=>"secret1" + } + + assert_equal expected_json, json + end + + test "fetch with non-default account and service account impersonation" do + stub_gcloud_version + stub_authenticated + stub_items(0, project: "some-project", version: "123", account: "email@example.com", impersonate_service_account: "service-user@example.com") + + json = JSON.parse(shellunescape(run_command("fetch", "some-project/item1/123", account: "email@example.com,service-user@example.com"))) + + expected_json = { + "some-project/item1"=>"secret1" + } + + assert_equal expected_json, json + end + + test "fetch without CLI installed" do + stub_gcloud_version(succeed: false) + + error = assert_raises RuntimeError do + JSON.parse(shellunescape(run_command("fetch", "item1"))) + end + assert_equal "gcloud CLI is not installed", error.message + end + + private + def run_command(*command, account: "default") + stdouted do + Kamal::Cli::Secrets.start \ + [ *command, + "-c", "test/fixtures/deploy_with_accessories.yml", + "--adapter", "gcp_secret_manager", + "--account", account ] + end + end + + def stub_gcloud_version(succeed: true) + stub_ticks_with("gcloud --version 2> /dev/null", succeed: succeed) + end + + def stub_authenticated + stub_ticks + .with("gcloud auth list --format=json") + .returns(<<~JSON) + [ + { + "account": "email@example.com", + "status": "ACTIVE" + } + ] + JSON + end + + def stub_unauthenticated + stub_ticks + .with("gcloud auth list --format=json") + .returns("[]") + end + + def stub_mypassword + stub_ticks + .with("gcloud secrets versions access latest --secret=mypassword --format=json") + .returns(<<~JSON) + { + "name": "projects/000000000/secrets/mypassword/versions/1", + "payload": { + "data": "c2VjcmV0MTIz", + "dataCrc32c": "2522602764" + } + } + JSON + end + + def stub_items(n, project: nil, account: nil, version: "latest", impersonate_service_account: nil) + payloads = [ + { data: "c2VjcmV0MQ==", checksum: 1846998209 }, + { data: "c2VjcmV0Mg==", checksum: 2101741365 }, + { data: "c2VjcmV0Mw==", checksum: 2402124854 } + ] + stub_ticks + .with("gcloud secrets versions access #{version} " \ + "--secret=item#{n + 1}" \ + "#{" --project=#{project}" if project}" \ + "#{" --account=#{account}" if account}" \ + "#{" --impersonate-service-account=#{impersonate_service_account}" if impersonate_service_account} " \ + "--format=json") + .returns(<<~JSON) + { + "name": "projects/000000001/secrets/item1/versions/1", + "payload": { + "data": "#{payloads[n][:data]}", + "dataCrc32c": "#{payloads[n][:checksum]}" + } + } + JSON + end +end From a07ef64fade353aec99a4b2c6715091a55e6991f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Laszlo?= Date: Wed, 20 Nov 2024 15:27:51 +0100 Subject: [PATCH 2/9] Fix --account documentation --- lib/kamal/secrets/adapters/gcp_secret_manager.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/kamal/secrets/adapters/gcp_secret_manager.rb b/lib/kamal/secrets/adapters/gcp_secret_manager.rb index c6d6387b..25c9fa00 100644 --- a/lib/kamal/secrets/adapters/gcp_secret_manager.rb +++ b/lib/kamal/secrets/adapters/gcp_secret_manager.rb @@ -1,10 +1,8 @@ class Kamal::Secrets::Adapters::GcpSecretManager < Kamal::Secrets::Adapters::Base private def login(account) - # Since only the account option is passed from the cli, we'll use it for: - # - Account - # - GCP project - # - Service account impersonation + # Since only the account option is passed from the cli, we'll use it for both account and service account + # impersonation. # # Syntax: # ACCOUNT: USER | USER "," DELEGATION_CHAIN From 0c9a367efcc11180e5930dc225e56ab3b9bd791f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Laszlo?= Date: Wed, 27 Nov 2024 13:33:04 +0100 Subject: [PATCH 3/9] Remove overly generic 'secret_manager' alias --- lib/kamal/secrets/adapters.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/kamal/secrets/adapters.rb b/lib/kamal/secrets/adapters.rb index 2abf29ad..e51c73ef 100644 --- a/lib/kamal/secrets/adapters.rb +++ b/lib/kamal/secrets/adapters.rb @@ -3,7 +3,7 @@ module Kamal::Secrets::Adapters def self.lookup(name) name = "one_password" if name.downcase == "1password" name = "last_pass" if name.downcase == "lastpass" - name = "gcp_secret_manager" if %w[gcp secret_manager].include? name.downcase + name = "gcp_secret_manager" if name.downcase == "gcp" adapter_class(name) end From 18f2aae9364638f33abdfddfa6aebe65a5c8b26f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Laszlo?= Date: Fri, 6 Dec 2024 17:15:22 +0100 Subject: [PATCH 4/9] Simplify parsing by changing account separators --- .../secrets/adapters/gcp_secret_manager.rb | 22 +++++-------------- .../gcp_secret_manager_adapter_test.rb | 6 ++--- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/lib/kamal/secrets/adapters/gcp_secret_manager.rb b/lib/kamal/secrets/adapters/gcp_secret_manager.rb index 25c9fa00..82fbb0bf 100644 --- a/lib/kamal/secrets/adapters/gcp_secret_manager.rb +++ b/lib/kamal/secrets/adapters/gcp_secret_manager.rb @@ -5,7 +5,7 @@ class Kamal::Secrets::Adapters::GcpSecretManager < Kamal::Secrets::Adapters::Bas # impersonation. # # Syntax: - # ACCOUNT: USER | USER "," DELEGATION_CHAIN + # ACCOUNT: USER | USER "|" DELEGATION_CHAIN # USER: DEFAULT_USER | EMAIL # DELEGATION_CHAIN: EMAIL | EMAIL "," DELEGATION_CHAIN # EMAIL: @@ -13,10 +13,10 @@ class Kamal::Secrets::Adapters::GcpSecretManager < Kamal::Secrets::Adapters::Bas # # Some valid examples: # - "my-user@example.com" sets the user - # - "my-user@example.com,my-service-user@example.com" will use my-user and enable service account impersonation as my-service-user + # - "my-user@example.com|my-service-user@example.com" will use my-user and enable service account impersonation as my-service-user # - "default" will use the default user and no impersonation - # - "default,my-service-user@example.com" will use the default user, and enable service account impersonation as my-service-user - # - "default,my-service-user@example.com,another-service-user@example.com" same as above, but with an impersonation delegation chain + # - "default|my-service-user@example.com" will use the default user, and enable service account impersonation as my-service-user + # - "default|my-service-user@example.com,another-service-user@example.com" same as above, but with an impersonation delegation chain if !logged_in? raise RuntimeError, "gcloud is not authenticated, please run `gcloud auth login`" @@ -102,19 +102,7 @@ class Kamal::Secrets::Adapters::GcpSecretManager < Kamal::Secrets::Adapters::Bas end def parse_account(account) - return "default", nil if account == "default" - - parts = account.split(",", 2) - - if parts.length == 2 - return parts.shift, parts.shift - elsif parts.length != 1 - raise RuntimeError, "Invalid account, too many parts: #{account}" - elsif is_user?(account) - return account, nil - end - - raise RuntimeError, "Invalid account, not a user: #{account}" + account.split("|", 2) end def is_user?(candidate) diff --git a/test/secrets/gcp_secret_manager_adapter_test.rb b/test/secrets/gcp_secret_manager_adapter_test.rb index 3369d08e..d9b30151 100644 --- a/test/secrets/gcp_secret_manager_adapter_test.rb +++ b/test/secrets/gcp_secret_manager_adapter_test.rb @@ -91,7 +91,7 @@ class GcpSecretManagerAdapterTest < SecretAdapterTestCase stub_authenticated stub_items(0, project: "some-project", version: "123", impersonate_service_account: "service-user@example.com") - json = JSON.parse(shellunescape(run_command("fetch", "some-project/item1/123", account: "default,service-user@example.com"))) + json = JSON.parse(shellunescape(run_command("fetch", "some-project/item1/123", account: "default|service-user@example.com"))) expected_json = { "some-project/item1"=>"secret1" @@ -105,7 +105,7 @@ class GcpSecretManagerAdapterTest < SecretAdapterTestCase stub_authenticated stub_items(0, project: "some-project", version: "123", account: "user@example.com", impersonate_service_account: "service-user@example.com,service-user2@example.com") - json = JSON.parse(shellunescape(run_command("fetch", "some-project/item1/123", account: "user@example.com,service-user@example.com,service-user2@example.com"))) + json = JSON.parse(shellunescape(run_command("fetch", "some-project/item1/123", account: "user@example.com|service-user@example.com,service-user2@example.com"))) expected_json = { "some-project/item1"=>"secret1" @@ -119,7 +119,7 @@ class GcpSecretManagerAdapterTest < SecretAdapterTestCase stub_authenticated stub_items(0, project: "some-project", version: "123", account: "email@example.com", impersonate_service_account: "service-user@example.com") - json = JSON.parse(shellunescape(run_command("fetch", "some-project/item1/123", account: "email@example.com,service-user@example.com"))) + json = JSON.parse(shellunescape(run_command("fetch", "some-project/item1/123", account: "email@example.com|service-user@example.com"))) expected_json = { "some-project/item1"=>"secret1" From ea170fbe5e29da40fb7ccb260ec2ccf27b747155 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Laszlo?= Date: Fri, 6 Dec 2024 17:22:03 +0100 Subject: [PATCH 5/9] Run gcloud auth login if user is not authenticated --- lib/kamal/secrets/adapters/gcp_secret_manager.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/kamal/secrets/adapters/gcp_secret_manager.rb b/lib/kamal/secrets/adapters/gcp_secret_manager.rb index 82fbb0bf..9945e4fb 100644 --- a/lib/kamal/secrets/adapters/gcp_secret_manager.rb +++ b/lib/kamal/secrets/adapters/gcp_secret_manager.rb @@ -19,7 +19,8 @@ class Kamal::Secrets::Adapters::GcpSecretManager < Kamal::Secrets::Adapters::Bas # - "default|my-service-user@example.com,another-service-user@example.com" same as above, but with an impersonation delegation chain if !logged_in? - raise RuntimeError, "gcloud is not authenticated, please run `gcloud auth login`" + `gcloud auth login` + raise RuntimeError, "gcloud is not authenticated, please run `gcloud auth login`" if !logged_in? end user, impersonate_service_account = parse_account(account) From dc64aaa0def7c42c7a7b88faa86ba6a4481e8ab1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Laszlo?= Date: Fri, 6 Dec 2024 17:32:01 +0100 Subject: [PATCH 6/9] Add gcloud auth login invocation to test --- test/secrets/gcp_secret_manager_adapter_test.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/secrets/gcp_secret_manager_adapter_test.rb b/test/secrets/gcp_secret_manager_adapter_test.rb index d9b30151..5d852df7 100644 --- a/test/secrets/gcp_secret_manager_adapter_test.rb +++ b/test/secrets/gcp_secret_manager_adapter_test.rb @@ -169,6 +169,15 @@ class GcpSecretManagerAdapterTest < SecretAdapterTestCase stub_ticks .with("gcloud auth list --format=json") .returns("[]") + + stub_ticks + .with("gcloud auth login") + .returns(<<~JSON) + { + "expired": false, + "valid": true + } + JSON end def stub_mypassword From 19b4359b17db53b96ffd8483b590fb4a88619917 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Laszlo?= Date: Fri, 6 Dec 2024 17:32:31 +0100 Subject: [PATCH 7/9] Use a nil session --- .../secrets/adapters/gcp_secret_manager.rb | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/lib/kamal/secrets/adapters/gcp_secret_manager.rb b/lib/kamal/secrets/adapters/gcp_secret_manager.rb index 9945e4fb..ada16c38 100644 --- a/lib/kamal/secrets/adapters/gcp_secret_manager.rb +++ b/lib/kamal/secrets/adapters/gcp_secret_manager.rb @@ -23,28 +23,29 @@ class Kamal::Secrets::Adapters::GcpSecretManager < Kamal::Secrets::Adapters::Bas raise RuntimeError, "gcloud is not authenticated, please run `gcloud auth login`" if !logged_in? end - user, impersonate_service_account = parse_account(account) - - { - user: user, - impersonate_service_account: impersonate_service_account - } + nil end def fetch_secrets(secrets, account:, session:) - # puts("secrets spec: #{secrets.inspect}") + user, service_account = parse_account(account) + {}.tap do |results| secrets_with_metadata(secrets).each do |secret, metadata| project, secret_name, secret_version = metadata item_name = project == "default" ? secret_name : "#{project}/#{secret_name}" - results[item_name] = fetch_secret(session, project, secret_name, secret_version) + results[item_name] = fetch_secret(project, secret_name, secret_version, user, service_account) raise RuntimeError, "Could not read #{item_name} from Google Secret Manager" unless $?.success? end end end - def fetch_secret(session, project, secret_name, secret_version) - secret = run_command("secrets versions access #{secret_version} --secret=#{secret_name.shellescape}", session: session, project: project) + def fetch_secret(project, secret_name, secret_version, user, service_account) + secret = run_command( + "secrets versions access #{secret_version} --secret=#{secret_name.shellescape}", + project: project, + user: user, + service_account: service_account + ) Base64.decode64(secret.dig("payload", "data")) end @@ -77,11 +78,11 @@ class Kamal::Secrets::Adapters::GcpSecretManager < Kamal::Secrets::Adapters::Bas end end - def run_command(command, session: nil, project: nil) + def run_command(command, project: "default", user: "default", service_account: nil) full_command = [ "gcloud", command ] full_command << "--project=#{project}" unless project == "default" - full_command << "--account=#{session[:user]}" unless session[:user] == "default" - full_command << "--impersonate-service-account=#{session[:impersonate_service_account]}" if session[:impersonate_service_account] + full_command << "--account=#{user}" unless user == "default" + full_command << "--impersonate-service-account=#{service_account}" if service_account full_command << "--format=json" full_command = full_command.join(" ") From eb82b4a753e42037730d68a3c4adac6de1beeca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Laszlo?= Date: Fri, 6 Dec 2024 17:40:08 +0100 Subject: [PATCH 8/9] Keep the 'default' prefix for secret items --- lib/kamal/secrets/adapters/gcp_secret_manager.rb | 5 ++--- test/secrets/gcp_secret_manager_adapter_test.rb | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/kamal/secrets/adapters/gcp_secret_manager.rb b/lib/kamal/secrets/adapters/gcp_secret_manager.rb index ada16c38..f78efbcd 100644 --- a/lib/kamal/secrets/adapters/gcp_secret_manager.rb +++ b/lib/kamal/secrets/adapters/gcp_secret_manager.rb @@ -30,9 +30,8 @@ class Kamal::Secrets::Adapters::GcpSecretManager < Kamal::Secrets::Adapters::Bas user, service_account = parse_account(account) {}.tap do |results| - secrets_with_metadata(secrets).each do |secret, metadata| - project, secret_name, secret_version = metadata - item_name = project == "default" ? secret_name : "#{project}/#{secret_name}" + secrets_with_metadata(secrets).each do |secret, (project, secret_name, secret_version)| + item_name = "#{project}/#{secret_name}" results[item_name] = fetch_secret(project, secret_name, secret_version, user, service_account) raise RuntimeError, "Could not read #{item_name} from Google Secret Manager" unless $?.success? end diff --git a/test/secrets/gcp_secret_manager_adapter_test.rb b/test/secrets/gcp_secret_manager_adapter_test.rb index 5d852df7..341f42e9 100644 --- a/test/secrets/gcp_secret_manager_adapter_test.rb +++ b/test/secrets/gcp_secret_manager_adapter_test.rb @@ -8,7 +8,7 @@ class GcpSecretManagerAdapterTest < SecretAdapterTestCase json = JSON.parse(shellunescape(run_command("fetch", "mypassword"))) - expected_json = { "mypassword"=>"secret123" } + expected_json = { "default/mypassword"=>"secret123" } assert_equal expected_json, json end From 8103d686888a1a3234c968d34258a50fb2df8426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Laszlo?= Date: Fri, 6 Dec 2024 17:43:41 +0100 Subject: [PATCH 9/9] Shellescape all interpolated strings in commands --- lib/kamal/secrets/adapters/gcp_secret_manager.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/kamal/secrets/adapters/gcp_secret_manager.rb b/lib/kamal/secrets/adapters/gcp_secret_manager.rb index f78efbcd..b8dfebf3 100644 --- a/lib/kamal/secrets/adapters/gcp_secret_manager.rb +++ b/lib/kamal/secrets/adapters/gcp_secret_manager.rb @@ -40,7 +40,7 @@ class Kamal::Secrets::Adapters::GcpSecretManager < Kamal::Secrets::Adapters::Bas def fetch_secret(project, secret_name, secret_version, user, service_account) secret = run_command( - "secrets versions access #{secret_version} --secret=#{secret_name.shellescape}", + "secrets versions access #{secret_version.shellescape} --secret=#{secret_name.shellescape}", project: project, user: user, service_account: service_account @@ -79,9 +79,9 @@ class Kamal::Secrets::Adapters::GcpSecretManager < Kamal::Secrets::Adapters::Bas def run_command(command, project: "default", user: "default", service_account: nil) full_command = [ "gcloud", command ] - full_command << "--project=#{project}" unless project == "default" - full_command << "--account=#{user}" unless user == "default" - full_command << "--impersonate-service-account=#{service_account}" if service_account + full_command << "--project=#{project.shellescape}" unless project == "default" + full_command << "--account=#{user.shellescape}" unless user == "default" + full_command << "--impersonate-service-account=#{service_account.shellescape}" if service_account full_command << "--format=json" full_command = full_command.join(" ")