From a004232ffc67ab3a173cb470d9193d623bd745fd Mon Sep 17 00:00:00 2001 From: Nick Coyne Date: Mon, 2 Jun 2025 09:06:05 +1200 Subject: [PATCH] Request secrets as json --- .../adapters/bitwarden_secrets_manager.rb | 24 ++-- .../bitwarden_secrets_manager_adapter_test.rb | 124 ++++++++++++++---- 2 files changed, 108 insertions(+), 40 deletions(-) diff --git a/lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb b/lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb index 66afbe70..0bae61d3 100644 --- a/lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb +++ b/lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb @@ -6,8 +6,8 @@ class Kamal::Secrets::Adapters::BitwardenSecretsManager < Kamal::Secrets::Adapte private LIST_ALL_SELECTOR = "all" LIST_ALL_FROM_PROJECT_SUFFIX = "/all" - LIST_COMMAND = "secret list -o env" - GET_COMMAND = "secret get -o env" + LIST_COMMAND = "secret list" + GET_COMMAND = "secret get" def fetch_secrets(secrets, from:, account:, session:) raise RuntimeError, "You must specify what to retrieve from Bitwarden Secrets Manager" if secrets.length == 0 @@ -18,17 +18,17 @@ class Kamal::Secrets::Adapters::BitwardenSecretsManager < Kamal::Secrets::Adapte {}.tap do |results| if command.nil? secrets.each do |secret_uuid| - secret = run_command("#{GET_COMMAND} #{secret_uuid.shellescape}") + item_json = run_command("#{GET_COMMAND} #{secret_uuid.shellescape}") raise RuntimeError, "Could not read #{secret_uuid} from Bitwarden Secrets Manager" unless $?.success? - key, value = parse_secret(secret) - results[key] = value + item_json = JSON.parse(item_json) + results[item_json["key"]] = item_json["value"] end else - secrets = run_command(command) + items_json = run_command(command) raise RuntimeError, "Could not read secrets from Bitwarden Secrets Manager" unless $?.success? - secrets.split("\n").each do |secret| - key, value = parse_secret(secret) - results[key] = value + + JSON.parse(items_json).each do |item_json| + results[item_json["key"]] = item_json["value"] end end end @@ -45,12 +45,6 @@ class Kamal::Secrets::Adapters::BitwardenSecretsManager < Kamal::Secrets::Adapte end end - def parse_secret(secret) - key, value = secret.split("=", 2) - value = value.gsub(/^"|"$/, "") - [ key, value ] - end - def run_command(command, session: nil) full_command = [ "bws", command ].join(" ") `#{full_command}` diff --git a/test/secrets/bitwarden_secrets_manager_adapter_test.rb b/test/secrets/bitwarden_secrets_manager_adapter_test.rb index 1723da42..ecb30ff0 100644 --- a/test/secrets/bitwarden_secrets_manager_adapter_test.rb +++ b/test/secrets/bitwarden_secrets_manager_adapter_test.rb @@ -15,57 +15,111 @@ class BitwardenSecretsManagerAdapterTest < SecretAdapterTestCase stub_ticks.with("bws --version 2> /dev/null") stub_login stub_ticks - .with("bws secret list -o env") - .returns("KAMAL_REGISTRY_PASSWORD=\"some_password\"\nMY_OTHER_SECRET=\"my=weird\"secret\"") + .with("bws secret list") + .returns(<<~JSON) + [ + { + "key": "KAMAL_REGISTRY_PASSWORD", + "value": "some_password" + }, + { + "key": "MY_OTHER_SECRET", + "value": "my=wierd\\"secret" + } + ] + JSON - expected = '{"KAMAL_REGISTRY_PASSWORD":"some_password","MY_OTHER_SECRET":"my\=weird\"secret"}' - actual = shellunescape(run_command("fetch", "all")) - assert_equal expected, actual + json = JSON.parse(shellunescape(run_command("fetch", "all"))) + + expected_json = { + "KAMAL_REGISTRY_PASSWORD"=>"some_password", + "MY_OTHER_SECRET"=>"my=wierd\"secret" + } + + assert_equal expected_json, json end test "fetch all with from" do stub_ticks.with("bws --version 2> /dev/null") stub_login stub_ticks - .with("bws secret list -o env 82aeb5bd-6958-4a89-8197-eacab758acce") - .returns("KAMAL_REGISTRY_PASSWORD=\"some_password\"\nMY_OTHER_SECRET=\"my=weird\"secret\"") + .with("bws secret list 82aeb5bd-6958-4a89-8197-eacab758acce") + .returns(<<~JSON) + [ + { + "key": "KAMAL_REGISTRY_PASSWORD", + "value": "some_password" + }, + { + "key": "MY_OTHER_SECRET", + "value": "my=wierd\\"secret" + } + ] + JSON - expected = '{"KAMAL_REGISTRY_PASSWORD":"some_password","MY_OTHER_SECRET":"my\=weird\"secret"}' - actual = shellunescape(run_command("fetch", "all", "--from", "82aeb5bd-6958-4a89-8197-eacab758acce")) - assert_equal expected, actual + json = JSON.parse(shellunescape(run_command("fetch", "all", "--from", "82aeb5bd-6958-4a89-8197-eacab758acce"))) + + expected_json = { + "KAMAL_REGISTRY_PASSWORD"=>"some_password", + "MY_OTHER_SECRET"=>"my=wierd\"secret" + } + + assert_equal expected_json, json end test "fetch item" do stub_ticks.with("bws --version 2> /dev/null") stub_login stub_ticks - .with("bws secret get -o env 82aeb5bd-6958-4a89-8197-eacab758acce") - .returns("KAMAL_REGISTRY_PASSWORD=\"some_password\"") + .with("bws secret get 82aeb5bd-6958-4a89-8197-eacab758acce") + .returns(<<~JSON) + { + "key": "KAMAL_REGISTRY_PASSWORD", + "value": "some_password" + } + JSON - expected = '{"KAMAL_REGISTRY_PASSWORD":"some_password"}' - actual = shellunescape(run_command("fetch", "82aeb5bd-6958-4a89-8197-eacab758acce")) - assert_equal expected, actual + json = JSON.parse(shellunescape(run_command("fetch", "82aeb5bd-6958-4a89-8197-eacab758acce"))) + expected_json = { + "KAMAL_REGISTRY_PASSWORD"=>"some_password" + } + + assert_equal expected_json, json end test "fetch with multiple items" do stub_ticks.with("bws --version 2> /dev/null") stub_login stub_ticks - .with("bws secret get -o env 82aeb5bd-6958-4a89-8197-eacab758acce") - .returns("KAMAL_REGISTRY_PASSWORD=\"some_password\"") + .with("bws secret get 82aeb5bd-6958-4a89-8197-eacab758acce") + .returns(<<~JSON) + { + "key": "KAMAL_REGISTRY_PASSWORD", + "value": "some_password" + } + JSON stub_ticks - .with("bws secret get -o env 6f8cdf27-de2b-4c77-a35d-07df8050e332") - .returns("MY_OTHER_SECRET=\"my=weird\"secret\"") + .with("bws secret get 6f8cdf27-de2b-4c77-a35d-07df8050e332") + .returns(<<~JSON) + { + "key": "MY_OTHER_SECRET", + "value": "my=wierd\\"secret" + } + JSON - expected = '{"KAMAL_REGISTRY_PASSWORD":"some_password","MY_OTHER_SECRET":"my\=weird\"secret"}' - actual = shellunescape(run_command("fetch", "82aeb5bd-6958-4a89-8197-eacab758acce", "6f8cdf27-de2b-4c77-a35d-07df8050e332")) - assert_equal expected, actual + json = JSON.parse(shellunescape(run_command("fetch", "82aeb5bd-6958-4a89-8197-eacab758acce", "6f8cdf27-de2b-4c77-a35d-07df8050e332"))) + expected_json = { + "KAMAL_REGISTRY_PASSWORD"=>"some_password", + "MY_OTHER_SECRET"=>"my=wierd\"secret" + } + + assert_equal expected_json, json end test "fetch all empty" do stub_ticks.with("bws --version 2> /dev/null") stub_login - stub_ticks_with("bws secret list -o env", succeed: false).returns("Error:\n0: Received error message from server") + stub_ticks_with("bws secret list", succeed: false).returns("Error:\n0: Received error message from server") error = assert_raises RuntimeError do (shellunescape(run_command("fetch", "all"))) @@ -76,8 +130,8 @@ class BitwardenSecretsManagerAdapterTest < SecretAdapterTestCase test "fetch nonexistent item" do stub_ticks.with("bws --version 2> /dev/null") stub_login - stub_ticks_with("bws secret get -o env 82aeb5bd-6958-4a89-8197-eacab758acce", succeed: false) - .returns("ERROR (RuntimeError): Could not read 82aeb5bd-6958-4a89-8197-eacab758acce from Bitwarden Secrets Manager") + stub_ticks_with("bws secret get 82aeb5bd-6958-4a89-8197-eacab758acce", succeed: false) + .returns("Error:\n0: Received error message from server") error = assert_raises RuntimeError do (shellunescape(run_command("fetch", "82aeb5bd-6958-4a89-8197-eacab758acce"))) @@ -85,6 +139,26 @@ class BitwardenSecretsManagerAdapterTest < SecretAdapterTestCase assert_equal("Could not read 82aeb5bd-6958-4a89-8197-eacab758acce from Bitwarden Secrets Manager", error.message) end + test "fetch item with linebreak in value" do + stub_ticks.with("bws --version 2> /dev/null") + stub_login + stub_ticks + .with("bws secret get 82aeb5bd-6958-4a89-8197-eacab758acce") + .returns(<<~JSON) + { + "key": "SSH_PRIVATE_KEY", + "value": "some_key\\nwith_linebreak" + } + JSON + + json = JSON.parse(shellunescape(run_command("fetch", "82aeb5bd-6958-4a89-8197-eacab758acce"))) + expected_json = { + "SSH_PRIVATE_KEY"=>"some_key\nwith_linebreak" + } + + assert_equal expected_json, json + end + test "fetch with no access token" do stub_ticks.with("bws --version 2> /dev/null") stub_ticks_with("bws run 'echo OK'", succeed: false)