From 8cec17dd058dbf446e67d7b85807a78c0dfc8309 Mon Sep 17 00:00:00 2001 From: Kyle Rippey Date: Tue, 15 Oct 2024 23:20:18 -0700 Subject: [PATCH] Made secret adapters raise a meaningful error if the required CLI is not installed --- lib/kamal/secrets/adapters/base.rb | 5 +++++ lib/kamal/secrets/adapters/bitwarden.rb | 9 +++++++++ lib/kamal/secrets/adapters/last_pass.rb | 9 +++++++++ lib/kamal/secrets/adapters/one_password.rb | 9 +++++++++ lib/kamal/secrets/adapters/test.rb | 4 ++++ test/secrets/bitwarden_adapter_test.rb | 23 ++++++++++++++++++++++ test/secrets/last_pass_adapter_test.rb | 13 ++++++++++++ test/secrets/one_password_adapter_test.rb | 15 ++++++++++++++ 8 files changed, 87 insertions(+) diff --git a/lib/kamal/secrets/adapters/base.rb b/lib/kamal/secrets/adapters/base.rb index 97b2a458e..579414aff 100644 --- a/lib/kamal/secrets/adapters/base.rb +++ b/lib/kamal/secrets/adapters/base.rb @@ -2,6 +2,7 @@ class Kamal::Secrets::Adapters::Base delegate :optionize, to: Kamal::Utils def fetch(secrets, account:, from: nil) + check_dependencies! session = login(account) full_secrets = secrets.map { |secret| [ from, secret ].compact.join("/") } fetch_secrets(full_secrets, account: account, session: session) @@ -15,4 +16,8 @@ def login(...) def fetch_secrets(...) raise NotImplementedError end + + def check_dependencies! + raise NotImplementedError + end end diff --git a/lib/kamal/secrets/adapters/bitwarden.rb b/lib/kamal/secrets/adapters/bitwarden.rb index e84a0d93e..7ea4f6f72 100644 --- a/lib/kamal/secrets/adapters/bitwarden.rb +++ b/lib/kamal/secrets/adapters/bitwarden.rb @@ -63,4 +63,13 @@ def run_command(command, session: nil, raw: false) result = `#{full_command}`.strip raw ? result : JSON.parse(result) end + + def check_dependencies! + raise RuntimeError, "Bitwarden CLI is not installed" unless cli_installed? + end + + def cli_installed? + `bw --version 2> /dev/null` + $?.success? + end end diff --git a/lib/kamal/secrets/adapters/last_pass.rb b/lib/kamal/secrets/adapters/last_pass.rb index 390e84edc..2f95148b0 100644 --- a/lib/kamal/secrets/adapters/last_pass.rb +++ b/lib/kamal/secrets/adapters/last_pass.rb @@ -27,4 +27,13 @@ def fetch_secrets(secrets, account:, session:) end end end + + def check_dependencies! + raise RuntimeError, "LastPass CLI is not installed" unless cli_installed? + end + + def cli_installed? + `lpass --version 2> /dev/null` + $?.success? + end end diff --git a/lib/kamal/secrets/adapters/one_password.rb b/lib/kamal/secrets/adapters/one_password.rb index c7e9b28df..c7f9d5abd 100644 --- a/lib/kamal/secrets/adapters/one_password.rb +++ b/lib/kamal/secrets/adapters/one_password.rb @@ -58,4 +58,13 @@ def op_item_get(vault, item, fields, account:, session:) raise RuntimeError, "Could not read #{fields.join(", ")} from #{item} in the #{vault} 1Password vault" unless $?.success? end end + + def check_dependencies! + raise RuntimeError, "1Password CLI is not installed" unless cli_installed? + end + + def cli_installed? + `op --version 2> /dev/null` + $?.success? + end end diff --git a/lib/kamal/secrets/adapters/test.rb b/lib/kamal/secrets/adapters/test.rb index fc0903d99..82577a762 100644 --- a/lib/kamal/secrets/adapters/test.rb +++ b/lib/kamal/secrets/adapters/test.rb @@ -7,4 +7,8 @@ def login(account) def fetch_secrets(secrets, account:, session:) secrets.to_h { |secret| [ secret, secret.reverse ] } end + + def check_dependencies! + # no op + end end diff --git a/test/secrets/bitwarden_adapter_test.rb b/test/secrets/bitwarden_adapter_test.rb index e2a3ac375..2bc871d38 100644 --- a/test/secrets/bitwarden_adapter_test.rb +++ b/test/secrets/bitwarden_adapter_test.rb @@ -2,6 +2,8 @@ class BitwardenAdapterTest < SecretAdapterTestCase test "fetch" do + stub_ticks.with("bw --version 2> /dev/null") + stub_unlocked stub_ticks.with("bw sync").returns("") stub_mypassword @@ -14,6 +16,8 @@ class BitwardenAdapterTest < SecretAdapterTestCase end test "fetch with no login" do + stub_ticks.with("bw --version 2> /dev/null") + stub_unlocked stub_ticks.with("bw sync").returns("") stub_noteitem @@ -25,6 +29,8 @@ class BitwardenAdapterTest < SecretAdapterTestCase end test "fetch with from" do + stub_ticks.with("bw --version 2> /dev/null") + stub_unlocked stub_ticks.with("bw sync").returns("") stub_myitem @@ -39,6 +45,8 @@ class BitwardenAdapterTest < SecretAdapterTestCase end test "fetch with multiple items" do + stub_ticks.with("bw --version 2> /dev/null") + stub_unlocked stub_ticks.with("bw sync").returns("") @@ -80,6 +88,8 @@ class BitwardenAdapterTest < SecretAdapterTestCase end test "fetch unauthenticated" do + stub_ticks.with("bw --version 2> /dev/null") + stub_ticks .with("bw status") .returns( @@ -101,6 +111,8 @@ class BitwardenAdapterTest < SecretAdapterTestCase end test "fetch locked" do + stub_ticks.with("bw --version 2> /dev/null") + stub_ticks .with("bw status") .returns( @@ -126,6 +138,8 @@ class BitwardenAdapterTest < SecretAdapterTestCase end test "fetch locked with session" do + stub_ticks.with("bw --version 2> /dev/null") + stub_ticks .with("bw status") .returns( @@ -150,6 +164,15 @@ class BitwardenAdapterTest < SecretAdapterTestCase assert_equal expected_json, json end + test "fetch without CLI installed" do + stub_ticks_with("bw --version 2> /dev/null", succeed: false) + + error = assert_raises RuntimeError do + JSON.parse(shellunescape(run_command("fetch", "mynote"))) + end + assert_equal "Bitwarden CLI is not installed", error.message + end + private def run_command(*command) stdouted do diff --git a/test/secrets/last_pass_adapter_test.rb b/test/secrets/last_pass_adapter_test.rb index 3801d486e..ca1f346ca 100644 --- a/test/secrets/last_pass_adapter_test.rb +++ b/test/secrets/last_pass_adapter_test.rb @@ -6,6 +6,7 @@ class LastPassAdapterTest < SecretAdapterTestCase end test "fetch" do + stub_ticks.with("lpass --version 2> /dev/null") stub_ticks.with("lpass status --color never").returns("Logged in as email@example.com.") stub_ticks @@ -63,6 +64,7 @@ class LastPassAdapterTest < SecretAdapterTestCase end test "fetch with from" do + stub_ticks.with("lpass --version 2> /dev/null") stub_ticks.with("lpass status --color never").returns("Logged in as email@example.com.") stub_ticks @@ -107,6 +109,8 @@ class LastPassAdapterTest < SecretAdapterTestCase end test "fetch with signin" do + stub_ticks.with("lpass --version 2> /dev/null") + stub_ticks_with("lpass status --color never", succeed: false).returns("Not logged in.") stub_ticks_with("lpass login email@example.com", succeed: true).returns("") stub_ticks.with("lpass show SECRET1 --json").returns(single_item_json) @@ -120,6 +124,15 @@ class LastPassAdapterTest < SecretAdapterTestCase assert_equal expected_json, json end + test "fetch without CLI installed" do + stub_ticks_with("lpass --version 2> /dev/null", succeed: false) + + error = assert_raises RuntimeError do + JSON.parse(shellunescape(run_command("fetch", "SECRET1", "FOLDER1/FSECRET1", "FOLDER1/FSECRET2"))) + end + assert_equal "LastPass CLI is not installed", error.message + end + private def run_command(*command) stdouted do diff --git a/test/secrets/one_password_adapter_test.rb b/test/secrets/one_password_adapter_test.rb index 59ad511db..36fab7c36 100644 --- a/test/secrets/one_password_adapter_test.rb +++ b/test/secrets/one_password_adapter_test.rb @@ -2,6 +2,7 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase test "fetch" do + stub_ticks.with("op --version 2> /dev/null") stub_ticks.with("op account get --account myaccount 2> /dev/null") stub_ticks @@ -56,6 +57,7 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase end test "fetch with multiple items" do + stub_ticks.with("op --version 2> /dev/null") stub_ticks.with("op account get --account myaccount 2> /dev/null") stub_ticks @@ -115,6 +117,8 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase end test "fetch with signin, no session" do + stub_ticks.with("op --version 2> /dev/null") + stub_ticks_with("op account get --account myaccount 2> /dev/null", succeed: false) stub_ticks_with("op signin --account \"myaccount\" --force --raw", succeed: true).returns("") @@ -132,6 +136,8 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase end test "fetch with signin and session" do + stub_ticks.with("op --version 2> /dev/null") + stub_ticks_with("op account get --account myaccount 2> /dev/null", succeed: false) stub_ticks_with("op signin --account \"myaccount\" --force --raw", succeed: true).returns("1234567890") @@ -148,6 +154,15 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase assert_equal expected_json, json end + test "fetch without CLI installed" do + stub_ticks_with("op --version 2> /dev/null", succeed: false) + + error = assert_raises RuntimeError do + JSON.parse(shellunescape(run_command("fetch", "--from", "op://myvault/myitem", "section/SECRET1", "section/SECRET2", "section2/SECRET3"))) + end + assert_equal "1Password CLI is not installed", error.message + end + private def run_command(*command) stdouted do