From cbf94fa7f5e38e648622215ae4af600808cb4595 Mon Sep 17 00:00:00 2001 From: capripot Date: Thu, 22 May 2025 21:59:53 -0700 Subject: [PATCH] feat: Add allowing retrieving all fields for an item With 1Password, there is a way to retrieve all fields of a given item directly without having to enumerate them. Allowing this when passing no arguments for secrets fetch command. --- lib/kamal/secrets/adapters/one_password.rb | 41 +++++++++++-- test/secrets/one_password_adapter_test.rb | 67 ++++++++++++++++++++-- 2 files changed, 97 insertions(+), 11 deletions(-) diff --git a/lib/kamal/secrets/adapters/one_password.rb b/lib/kamal/secrets/adapters/one_password.rb index fe454342..554ef167 100644 --- a/lib/kamal/secrets/adapters/one_password.rb +++ b/lib/kamal/secrets/adapters/one_password.rb @@ -16,10 +16,12 @@ class Kamal::Secrets::Adapters::OnePassword < Kamal::Secrets::Adapters::Base end def fetch_secrets(secrets, from:, account:, session:) + return fetch_all_secrets(from: from, account: account, session: session) if secrets.blank? + {}.tap do |results| vaults_items_fields(prefixed_secrets(secrets, from: from)).map do |vault, items| items.each do |item, fields| - fields_json = JSON.parse(op_item_get(vault, item, fields, account: account, session: session)) + fields_json = JSON.parse(op_item_get(vault, item, fields: fields, account: account, session: session)) fields_json = [ fields_json ] if fields.one? fields_json.each do |field_json| @@ -32,6 +34,23 @@ class Kamal::Secrets::Adapters::OnePassword < Kamal::Secrets::Adapters::Base end end + def fetch_all_secrets(from:, account:, session:) + {}.tap do |results| + vault_items(from).map do |vault, items| + items.each do |item| + + fields_json = JSON.parse(op_item_get(vault, item, account: account, session: session)).fetch("fields") + + fields_json.each do |field_json| + # The reference is in the form `op://vault/item/field[/field]` + field = field_json["reference"].delete_prefix("op://").delete_suffix("/password") + results[field] = field_json["value"] + end + end + end + end + end + def to_options(**options) optionize(options.compact).join(" ") end @@ -50,12 +69,22 @@ class Kamal::Secrets::Adapters::OnePassword < Kamal::Secrets::Adapters::Base end end - def op_item_get(vault, item, fields, account:, session:) - labels = fields.map { |field| "label=#{field}" }.join(",") - options = to_options(vault: vault, fields: labels, format: "json", account: account, session: session.presence) + def vault_items(from) + from = from.delete_prefix("op://") + vault, item = from.split("/") + { vault => [ item ]} + end - `op item get #{item.shellescape} #{options}`.tap do - raise RuntimeError, "Could not read #{fields.join(", ")} from #{item} in the #{vault} 1Password vault" unless $?.success? + def op_item_get(vault, item, fields: nil, account:, session:) + options = { vault: vault, format: "json", account: account, session: session.presence } + + if fields.present? + labels = fields.map { |field| "label=#{field}" }.join(",") + options.merge!(fields: labels) + end + + `op item get #{item.shellescape} #{to_options(**options)}`.tap do + raise RuntimeError, "Could not read from #{item} in the #{vault} 1Password vault" unless $?.success? end end diff --git a/test/secrets/one_password_adapter_test.rb b/test/secrets/one_password_adapter_test.rb index 36fab7c3..0f3d1199 100644 --- a/test/secrets/one_password_adapter_test.rb +++ b/test/secrets/one_password_adapter_test.rb @@ -6,7 +6,7 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase stub_ticks.with("op account get --account myaccount 2> /dev/null") stub_ticks - .with("op item get myitem --vault \"myvault\" --fields \"label=section.SECRET1,label=section.SECRET2,label=section2.SECRET3\" --format \"json\" --account \"myaccount\"") + .with("op item get myitem --vault \"myvault\" --format \"json\" --account \"myaccount\" --fields \"label=section.SECRET1,label=section.SECRET2,label=section2.SECRET3\"") .returns(<<~JSON) [ { @@ -61,7 +61,7 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase stub_ticks.with("op account get --account myaccount 2> /dev/null") stub_ticks - .with("op item get myitem --vault \"myvault\" --fields \"label=section.SECRET1,label=section.SECRET2\" --format \"json\" --account \"myaccount\"") + .with("op item get myitem --vault \"myvault\" --format \"json\" --account \"myaccount\" --fields \"label=section.SECRET1,label=section.SECRET2\"") .returns(<<~JSON) [ { @@ -90,7 +90,7 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase JSON stub_ticks - .with("op item get myitem2 --vault \"myvault\" --fields \"label=section2.SECRET3\" --format \"json\" --account \"myaccount\"") + .with("op item get myitem2 --vault \"myvault\" --format \"json\" --account \"myaccount\" --fields \"label=section2.SECRET3\"") .returns(<<~JSON) { "id": "aaaaaaaaaaaaaaaaaaaaaaaaaa", @@ -116,6 +116,63 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase assert_equal expected_json, json end + test "fetch all fields" do + stub_ticks.with("op --version 2> /dev/null") + stub_ticks.with("op account get --account myaccount 2> /dev/null") + + stub_ticks + .with("op item get myitem --vault \"myvault\" --format \"json\" --account \"myaccount\"") + .returns(<<~JSON) + { + "id": "ucbtiii777", + "title": "A title", + "version": 45, + "vault": { + "id": "vu7ki98do", + "name": "Vault" + }, + "category": "LOGIN", + "last_edited_by": "ABCT3684BC", + "created_at": "2025-05-22T06:47:01Z", + "updated_at": "2025-05-22T00:36:48.02598-07:00", + "additional_information": "—", + "fields": [ + { + "id": "aaaaaaaaaaaaaaaaaaaaaaaaaa", + "section": { + "id": "cccccccccccccccccccccccccc", + "label": "section" + }, + "type": "CONCEALED", + "label": "SECRET1", + "value": "VALUE1", + "reference": "op://myvault/myitem/section/SECRET1" + }, + { + "id": "bbbbbbbbbbbbbbbbbbbbbbbbbb", + "section": { + "id": "cccccccccccccccccccccccccc", + "label": "section" + }, + "type": "CONCEALED", + "label": "SECRET2", + "value": "VALUE2", + "reference": "op://myvault/myitem/section/SECRET2" + } + ] + } + JSON + + json = JSON.parse(shellunescape(run_command("fetch", "--from", "op://myvault/myitem"))) + + expected_json = { + "myvault/myitem/section/SECRET1"=>"VALUE1", + "myvault/myitem/section/SECRET2"=>"VALUE2", + } + + assert_equal expected_json, json + end + test "fetch with signin, no session" do stub_ticks.with("op --version 2> /dev/null") @@ -123,7 +180,7 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase stub_ticks_with("op signin --account \"myaccount\" --force --raw", succeed: true).returns("") stub_ticks - .with("op item get myitem --vault \"myvault\" --fields \"label=section.SECRET1\" --format \"json\" --account \"myaccount\"") + .with("op item get myitem --vault \"myvault\" --format \"json\" --account \"myaccount\" --fields \"label=section.SECRET1\"") .returns(single_item_json) json = JSON.parse(shellunescape(run_command("fetch", "--from", "op://myvault/myitem", "section/SECRET1"))) @@ -142,7 +199,7 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase stub_ticks_with("op signin --account \"myaccount\" --force --raw", succeed: true).returns("1234567890") stub_ticks - .with("op item get myitem --vault \"myvault\" --fields \"label=section.SECRET1\" --format \"json\" --account \"myaccount\" --session \"1234567890\"") + .with("op item get myitem --vault \"myvault\" --format \"json\" --account \"myaccount\" --session \"1234567890\" --fields \"label=section.SECRET1\"") .returns(single_item_json) json = JSON.parse(shellunescape(run_command("fetch", "--from", "op://myvault/myitem", "section/SECRET1")))