diff --git a/lib/kamal/secrets/adapters/bitwarden.rb b/lib/kamal/secrets/adapters/bitwarden.rb index 7ea4f6f7..5dfb72db 100644 --- a/lib/kamal/secrets/adapters/bitwarden.rb +++ b/lib/kamal/secrets/adapters/bitwarden.rb @@ -25,18 +25,15 @@ def fetch_secrets(secrets, account:, session:) {}.tap do |results| items_fields(secrets).each do |item, fields| item_json = run_command("get item #{item.shellescape}", session: session, raw: true) - raise RuntimeError, "Could not read #{secret} from Bitwarden" unless $?.success? + raise RuntimeError, "Could not read #{item} from Bitwarden" unless $?.success? item_json = JSON.parse(item_json) - if fields.any? - fields.each do |field| - item_field = item_json["fields"].find { |f| f["name"] == field } - raise RuntimeError, "Could not find field #{field} in item #{item} in Bitwarden" unless item_field - value = item_field["value"] - results["#{item}/#{field}"] = value - end + results.merge! fetch_secrets_from_fields(fields, item, item_json) elsif item_json.dig("login", "password") results[item] = item_json.dig("login", "password") + elsif item_json["fields"]&.any? + fields = item_json["fields"].pluck("name") + results.merge! fetch_secrets_from_fields(fields, item, item_json) else raise RuntimeError, "Item #{item} is not a login type item and no fields were specified" end @@ -44,6 +41,15 @@ def fetch_secrets(secrets, account:, session:) end end + def fetch_secrets_from_fields(fields, item, item_json) + fields.to_h do |field| + item_field = item_json["fields"].find { |f| f["name"] == field } + raise RuntimeError, "Could not find field #{field} in item #{item} in Bitwarden" unless item_field + value = item_field["value"] + [ "#{item}/#{field}", value ] + end + end + def items_fields(secrets) {}.tap do |items| secrets.each do |secret| diff --git a/test/secrets/bitwarden_adapter_test.rb b/test/secrets/bitwarden_adapter_test.rb index 2bc871d3..ad280791 100644 --- a/test/secrets/bitwarden_adapter_test.rb +++ b/test/secrets/bitwarden_adapter_test.rb @@ -44,6 +44,23 @@ class BitwardenAdapterTest < SecretAdapterTestCase assert_equal expected_json, json end + test "fetch all with from" do + stub_ticks.with("bw --version 2> /dev/null") + + stub_unlocked + stub_ticks.with("bw sync").returns("") + stub_noteitem_with_fields + + json = JSON.parse(shellunescape(run_command("fetch", "mynotefields"))) + + expected_json = { + "mynotefields/field1"=>"secret1", "mynotefields/field2"=>"blam", "mynotefields/field3"=>"fewgrwjgk", + "mynotefields/field4"=>"auto" + } + + assert_equal expected_json, json + end + test "fetch with multiple items" do stub_ticks.with("bw --version 2> /dev/null") @@ -237,7 +254,37 @@ def stub_noteitem(session: nil) "collectionIds":[] } JSON - end + end + + def stub_noteitem_with_fields(session: nil) + stub_ticks + .with("#{"BW_SESSION=#{session} " if session}bw get item mynotefields") + .returns(<<~JSON) + { + "passwordHistory":null, + "revisionDate":"2024-09-28T09:07:27.461Z", + "creationDate":"2024-09-28T09:07:00.740Z", + "deletedDate":null, + "object":"item", + "id":"aaaaaaaa-cccc-eeee-0000-222222222222", + "organizationId":null, + "folderId":null, + "type":2, + "reprompt":0, + "name":"noteitem", + "notes":"NOTES", + "favorite":false, + "fields":[ + {"name":"field1","value":"secret1","type":1,"linkedId":null}, + {"name":"field2","value":"blam","type":1,"linkedId":null}, + {"name":"field3","value":"fewgrwjgk","type":1,"linkedId":null}, + {"name":"field4","value":"auto","type":1,"linkedId":null} + ], + "secureNote":{"type":0}, + "collectionIds":[] + } + JSON + end def stub_myitem stub_ticks @@ -260,7 +307,8 @@ def stub_myitem "fields":[ {"name":"field1","value":"secret1","type":1,"linkedId":null}, {"name":"field2","value":"blam","type":1,"linkedId":null}, - {"name":"field3","value":"fewgrwjgk","type":1,"linkedId":null} + {"name":"field3","value":"fewgrwjgk","type":1,"linkedId":null}, + {"name":"field4","value":"auto","type":1,"linkedId":null} ], "login":{"fido2Credentials":[],"uris":[],"username":null,"password":null,"totp":null,"passwordRevisionDate":null},"collectionIds":[] }