From 9b63ad5cb8eb648c2a2d4669e889ff5683711dea Mon Sep 17 00:00:00 2001 From: acidtib Date: Thu, 20 Feb 2025 22:38:07 -0700 Subject: [PATCH 1/5] feat: add Passbolt adapter --- lib/kamal/secrets/adapters/passbolt.rb | 60 ++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 lib/kamal/secrets/adapters/passbolt.rb diff --git a/lib/kamal/secrets/adapters/passbolt.rb b/lib/kamal/secrets/adapters/passbolt.rb new file mode 100644 index 00000000..b0eb1b10 --- /dev/null +++ b/lib/kamal/secrets/adapters/passbolt.rb @@ -0,0 +1,60 @@ +class Kamal::Secrets::Adapters::Passbolt < Kamal::Secrets::Adapters::Base + def requires_account? + false + end + + private + + def login(*) + `passbolt verify` + raise RuntimeError, "Failed to login to Passbolt" unless $?.success? + end + + def fetch_secrets(secrets, from:, **) + secrets = prefixed_secrets(secrets, from: from) + flags = secrets_get_flags(secrets) + secret_names = secrets.collect { |s| s.split("/").last } + + filter_condition = secret_names.any? ? "--filter '#{secret_names.map { |name| "Name == #{name.shellescape.inspect}" }.join(" || ")}'" : "" + items = `passbolt list resources #{filter_condition} #{flags} --json` + raise RuntimeError, "Could not read #{secrets} from Passbolt" unless $?.success? + + items = JSON.parse(items) + found_names = items.map { |item| item["name"] } + missing_secrets = secret_names - found_names + raise RuntimeError, "Could not find the following secrets in Passbolt: #{missing_secrets.join(", ")}" if missing_secrets.any? + + items.to_h { |item| [item["name"], item["password"]] } + end + + def secrets_get_flags(secrets) + folders = secrets + .select { |s| s.include?("/") } + .map { |s| s.split("/").first } + .uniq + + if folders.any? + folder_ids = folders.map do |folder| + fetch_folder = `passbolt list folders --filter 'Name == \"#{folder.shellescape}\"' --json` + raise RuntimeError, "Could not read folder #{folder} from Passbolt" unless $?.success? + + folder_items = JSON.parse(fetch_folder) + folder_item = folder_items.find { |item| item["name"] == folder } + folder_item["id"] + end + + "--folder #{folder_ids.join(" --folder ")}" + else + "" + end + end + + def check_dependencies! + raise RuntimeError, "Passbolt CLI is not installed" unless cli_installed? + end + + def cli_installed? + `passbolt --version 2> /dev/null` + $?.success? + end +end \ No newline at end of file From 913f07bbf212e3c7bd029cc4766c44e2e93012d4 Mon Sep 17 00:00:00 2001 From: acidtib Date: Fri, 21 Feb 2025 00:34:10 -0700 Subject: [PATCH 2/5] add PassboltAdapter tests --- test/secrets/passbolt_adapter.rb | 293 +++++++++++++++++++++++++++++++ 1 file changed, 293 insertions(+) create mode 100644 test/secrets/passbolt_adapter.rb diff --git a/test/secrets/passbolt_adapter.rb b/test/secrets/passbolt_adapter.rb new file mode 100644 index 00000000..1bb7c3b4 --- /dev/null +++ b/test/secrets/passbolt_adapter.rb @@ -0,0 +1,293 @@ +require "test_helper" + +class PassboltAdapterTest < SecretAdapterTestCase + setup do + `true` # Ensure $? is 0 + end + + test "fetch" do + stub_ticks_with("passbolt --version 2> /dev/null", succeed: true) + stub_ticks.with("passbolt verify 2> /dev/null", succeed: true) + + stub_ticks + .with("passbolt list resources --filter 'Name == \"SECRET1\" || Name == \"FSECRET1\" || Name == \"FSECRET2\"' --json") + .returns(<<~JSON) + [ + { + "id": "4c116996-f6d0-4342-9572-0d676f75b3ac", + "folder_parent_id": "", + "name": "FSECRET1", + "username": "", + "uri": "", + "password": "fsecret1", + "description": "", + "created_timestamp": "2025-02-21T06:04:29Z", + "modified_timestamp": "2025-02-21T06:04:29Z" + }, + { + "id": "62949b26-4957-43fe-9523-294d66861499", + "folder_parent_id": "", + "name": "FSECRET2", + "username": "", + "uri": "", + "password": "fsecret2", + "description": "", + "created_timestamp": "2025-02-21T06:04:34Z", + "modified_timestamp": "2025-02-21T06:04:34Z" + }, + { + "id": "dd32963c-0db5-4303-a6fc-22c5229dabef", + "folder_parent_id": "", + "name": "SECRET1", + "username": "", + "uri": "", + "password": "secret1", + "description": "", + "created_timestamp": "2025-02-21T06:04:23Z", + "modified_timestamp": "2025-02-21T06:04:23Z" + } + ] + JSON + + json = JSON.parse( + shellunescape run_command("fetch", "SECRET1", "FSECRET1", "FSECRET2") + ) + + expected_json = { + "SECRET1"=>"secret1", + "FSECRET1"=>"fsecret1", + "FSECRET2"=>"fsecret2" + } + + assert_equal expected_json, json + end + + test "fetch with --from" do + stub_ticks_with("passbolt --version 2> /dev/null", succeed: true) + stub_ticks.with("passbolt verify 2> /dev/null", succeed: true) + + stub_ticks + .with("passbolt list folders --filter 'Name == \"my-project\"' --json") + .returns(folder_my_project_json) + + stub_ticks + .with("passbolt list resources --filter 'Name == \"SECRET1\" || Name == \"FSECRET1\" || Name == \"FSECRET2\"' --folder dcbe0e39-42d8-42db-9637-8256b9f2f8e3 --json") + .returns(<<~JSON) + [ + { + "id": "4c116996-f6d0-4342-9572-0d676f75b3ac", + "folder_parent_id": "dcbe0e39-42d8-42db-9637-8256b9f2f8e3", + "name": "FSECRET1", + "username": "", + "uri": "", + "password": "fsecret1", + "description": "", + "created_timestamp": "2025-02-21T06:04:29Z", + "modified_timestamp": "2025-02-21T06:04:29Z" + }, + { + "id": "62949b26-4957-43fe-9523-294d66861499", + "folder_parent_id": "dcbe0e39-42d8-42db-9637-8256b9f2f8e3", + "name": "FSECRET2", + "username": "", + "uri": "", + "password": "fsecret2", + "description": "", + "created_timestamp": "2025-02-21T06:04:34Z", + "modified_timestamp": "2025-02-21T06:04:34Z" + }, + { + "id": "dd32963c-0db5-4303-a6fc-22c5229dabef", + "folder_parent_id": "dcbe0e39-42d8-42db-9637-8256b9f2f8e3", + "name": "SECRET1", + "username": "", + "uri": "", + "password": "secret1", + "description": "", + "created_timestamp": "2025-02-21T06:04:23Z", + "modified_timestamp": "2025-02-21T06:04:23Z" + } + ] + JSON + + json = JSON.parse( + shellunescape run_command("fetch", "--from", "my-project", "SECRET1", "FSECRET1", "FSECRET2") + ) + + expected_json = { + "SECRET1"=>"secret1", + "FSECRET1"=>"fsecret1", + "FSECRET2"=>"fsecret2" + } + + assert_equal expected_json, json + end + + test "fetch with folder in secret" do + stub_ticks_with("passbolt --version 2> /dev/null", succeed: true) + stub_ticks.with("passbolt verify 2> /dev/null", succeed: true) + + stub_ticks + .with("passbolt list folders --filter 'Name == \"my-project\"' --json") + .returns(folder_my_project_json) + + stub_ticks + .with("passbolt list resources --filter 'Name == \"SECRET1\" || Name == \"FSECRET1\" || Name == \"FSECRET2\"' --folder dcbe0e39-42d8-42db-9637-8256b9f2f8e3 --json") + .returns(<<~JSON) + [ + { + "id": "4c116996-f6d0-4342-9572-0d676f75b3ac", + "folder_parent_id": "dcbe0e39-42d8-42db-9637-8256b9f2f8e3", + "name": "FSECRET1", + "username": "", + "uri": "", + "password": "fsecret1", + "description": "", + "created_timestamp": "2025-02-21T06:04:29Z", + "modified_timestamp": "2025-02-21T06:04:29Z" + }, + { + "id": "62949b26-4957-43fe-9523-294d66861499", + "folder_parent_id": "dcbe0e39-42d8-42db-9637-8256b9f2f8e3", + "name": "FSECRET2", + "username": "", + "uri": "", + "password": "fsecret2", + "description": "", + "created_timestamp": "2025-02-21T06:04:34Z", + "modified_timestamp": "2025-02-21T06:04:34Z" + }, + { + "id": "dd32963c-0db5-4303-a6fc-22c5229dabef", + "folder_parent_id": "dcbe0e39-42d8-42db-9637-8256b9f2f8e3", + "name": "SECRET1", + "username": "", + "uri": "", + "password": "secret1", + "description": "", + "created_timestamp": "2025-02-21T06:04:23Z", + "modified_timestamp": "2025-02-21T06:04:23Z" + } + ] + JSON + + json = JSON.parse( + shellunescape run_command("fetch", "my-project/SECRET1", "my-project/FSECRET1", "my-project/FSECRET2") + ) + + expected_json = { + "SECRET1"=>"secret1", + "FSECRET1"=>"fsecret1", + "FSECRET2"=>"fsecret2" + } + + assert_equal expected_json, json + end + + test "fetch from multiple folders" do + stub_ticks_with("passbolt --version 2> /dev/null", succeed: true) + stub_ticks.with("passbolt verify 2> /dev/null", succeed: true) + + stub_ticks + .with("passbolt list folders --filter 'Name == \"my-project\"' --json") + .returns(folder_my_project_json) + + stub_ticks + .with("passbolt list folders --filter 'Name == \"other-project\"' --json") + .returns(folder_other_project_json) + + stub_ticks + .with("passbolt list resources --filter 'Name == \"SECRET1\" || Name == \"FSECRET1\" || Name == \"FSECRET2\"' --folder dcbe0e39-42d8-42db-9637-8256b9f2f8e3 --folder 14e11dd8-b279-4689-8bd9-fa33ebb527da --json") + .returns(<<~JSON) + [ + { + "id": "4c116996-f6d0-4342-9572-0d676f75b3ac", + "folder_parent_id": "dcbe0e39-42d8-42db-9637-8256b9f2f8e3", + "name": "FSECRET1", + "username": "", + "uri": "", + "password": "fsecret1", + "description": "", + "created_timestamp": "2025-02-21T06:04:29Z", + "modified_timestamp": "2025-02-21T06:04:29Z" + }, + { + "id": "62949b26-4957-43fe-9523-294d66861499", + "folder_parent_id": "14e11dd8-b279-4689-8bd9-fa33ebb527da", + "name": "FSECRET2", + "username": "", + "uri": "", + "password": "fsecret2", + "description": "", + "created_timestamp": "2025-02-21T06:04:34Z", + "modified_timestamp": "2025-02-21T06:04:34Z" + }, + { + "id": "dd32963c-0db5-4303-a6fc-22c5229dabef", + "folder_parent_id": "dcbe0e39-42d8-42db-9637-8256b9f2f8e3", + "name": "SECRET1", + "username": "", + "uri": "", + "password": "secret1", + "description": "", + "created_timestamp": "2025-02-21T06:04:23Z", + "modified_timestamp": "2025-02-21T06:04:23Z" + } + ] + JSON + + json = JSON.parse( + shellunescape run_command("fetch", "my-project/SECRET1", "my-project/FSECRET1", "other-project/FSECRET2") + ) + + expected_json = { + "SECRET1"=>"secret1", + "FSECRET1"=>"fsecret1", + "FSECRET2"=>"fsecret2" + } + + assert_equal expected_json, json + end + + test "fetch without CLI installed" do + stub_ticks_with("passbolt --version 2> /dev/null", succeed: false) + + error = assert_raises RuntimeError do + JSON.parse(shellunescape(run_command("fetch", "HOST", "PORT"))) + end + + assert_equal "Passbolt CLI is not installed", error.message + end + + private + def run_command(*command) + stdouted do + Kamal::Cli::Secrets.start \ + [ *command, + "-c", "test/fixtures/deploy_with_accessories.yml", + "--adapter", "passbolt" ] + end + end + + def folder_my_project_json + <<~JSON + [ + { + "id": "dcbe0e39-42d8-42db-9637-8256b9f2f8e3", + "name": "my-project" + } + ] + JSON + end + + def folder_other_project_json + <<~JSON + [ + { + "id": "14e11dd8-b279-4689-8bd9-fa33ebb527da", + "name": "other-project" + } + ] + JSON + end +end \ No newline at end of file From 104914bf14fd2c53ffb6193e831153410852ed41 Mon Sep 17 00:00:00 2001 From: acidtib Date: Fri, 21 Feb 2025 17:04:04 -0700 Subject: [PATCH 3/5] refactor: improve retrieval logic for nested folders --- lib/kamal/secrets/adapters/passbolt.rb | 106 ++++++++++++++++++++----- 1 file changed, 88 insertions(+), 18 deletions(-) diff --git a/lib/kamal/secrets/adapters/passbolt.rb b/lib/kamal/secrets/adapters/passbolt.rb index b0eb1b10..90e9ee27 100644 --- a/lib/kamal/secrets/adapters/passbolt.rb +++ b/lib/kamal/secrets/adapters/passbolt.rb @@ -12,11 +12,42 @@ class Kamal::Secrets::Adapters::Passbolt < Kamal::Secrets::Adapters::Base def fetch_secrets(secrets, from:, **) secrets = prefixed_secrets(secrets, from: from) - flags = secrets_get_flags(secrets) - secret_names = secrets.collect { |s| s.split("/").last } + raise ArgumentError, "No secrets given to fetch" if secrets.empty? - filter_condition = secret_names.any? ? "--filter '#{secret_names.map { |name| "Name == #{name.shellescape.inspect}" }.join(" || ")}'" : "" - items = `passbolt list resources #{filter_condition} #{flags} --json` + secret_names = secrets.collect { |s| s.split("/").last } + folders = secrets_get_folders(secrets) + + # build filter conditions for each secret with its corresponding folder + filter_conditions = [] + secrets.each do |secret| + parts = secret.split("/") + secret_name = parts.last + + if parts.size > 1 + # get the folder path without the secret name + folder_path = parts[0..-2] + + # find the most nested folder for this path + current_folder = nil + current_path = [] + + folder_path.each do |folder_name| + current_path << folder_name + matching_folders = folders.select { |f| get_folder_path(f, folders) == current_path.join("/") } + current_folder = matching_folders.first if matching_folders.any? + end + + if current_folder + filter_conditions << "(Name == #{secret_name.shellescape.inspect} && FolderParentID == #{current_folder["id"].shellescape.inspect})" + end + else + # for root level secrets (no folders) + filter_conditions << "Name == #{secret_name.shellescape.inspect}" + end + end + + filter_condition = filter_conditions.any? ? "--filter '#{filter_conditions.join(" || ")}'" : "" + items = `passbolt list resources #{filter_condition} #{folders.map { |item| "--folder #{item["id"]}" }.join(" ")} --json` raise RuntimeError, "Could not read #{secrets} from Passbolt" unless $?.success? items = JSON.parse(items) @@ -27,26 +58,65 @@ class Kamal::Secrets::Adapters::Passbolt < Kamal::Secrets::Adapters::Base items.to_h { |item| [item["name"], item["password"]] } end - def secrets_get_flags(secrets) - folders = secrets + def secrets_get_folders(secrets) + # extract all folder paths (both parent and nested) + folder_paths = secrets .select { |s| s.include?("/") } - .map { |s| s.split("/").first } + .map { |s| s.split("/")[0..-2] } # get all parts except the secret name .uniq - if folders.any? - folder_ids = folders.map do |folder| - fetch_folder = `passbolt list folders --filter 'Name == \"#{folder.shellescape}\"' --json` - raise RuntimeError, "Could not read folder #{folder} from Passbolt" unless $?.success? + return [] if folder_paths.empty? - folder_items = JSON.parse(fetch_folder) - folder_item = folder_items.find { |item| item["name"] == folder } - folder_item["id"] + all_folders = [] + + # first get all top-level folders + parent_folders = folder_paths.map(&:first).uniq + filter_condition = "--filter '#{parent_folders.map { |name| "Name == #{name.shellescape.inspect}" }.join(" || ")}'" + fetch_folders = `passbolt list folders #{filter_condition} --json` + raise RuntimeError, "Could not read folders from Passbolt" unless $?.success? + + parent_folder_items = JSON.parse(fetch_folders) + all_folders.concat(parent_folder_items) + + # get nested folders for each parent + folder_paths.each do |path| + next if path.size <= 1 # skip non-nested folders + + parent = path[0] + parent_folder = parent_folder_items.find { |f| f["name"] == parent } + next unless parent_folder + + # for each nested level, get the folders using the parent's ID + current_parent = parent_folder + path[1..-1].each do |folder_name| + filter_condition = "--filter 'Name == #{folder_name.shellescape.inspect} && FolderParentID == #{current_parent["id"].shellescape.inspect}'" + fetch_nested = `passbolt list folders #{filter_condition} --json` + next unless $?.success? + + nested_folders = JSON.parse(fetch_nested) + break if nested_folders.empty? + + all_folders.concat(nested_folders) + current_parent = nested_folders.first end - - "--folder #{folder_ids.join(" --folder ")}" - else - "" end + + # check if we found all required folders + found_paths = all_folders.map { |f| get_folder_path(f, all_folders) } + missing_paths = folder_paths.map { |path| path.join("/") } - found_paths + raise RuntimeError, "Could not find the following folders in Passbolt: #{missing_paths.join(", ")}" if missing_paths.any? + + all_folders + end + + def get_folder_path(folder, all_folders, path = []) + path.unshift(folder["name"]) + return path.join("/") if folder["folder_parent_id"].to_s.empty? + + parent = all_folders.find { |f| f["id"] == folder["folder_parent_id"] } + return path.join("/") unless parent + + get_folder_path(parent, all_folders, path) end def check_dependencies! From 8acd35c4b747fa728afea1cda79d2592f79e03ea Mon Sep 17 00:00:00 2001 From: acidtib Date: Fri, 21 Feb 2025 17:04:46 -0700 Subject: [PATCH 4/5] test: add fetch functionality for nested folders and secrets --- test/secrets/passbolt_adapter.rb | 247 ++++++++++++++++++++++++++----- 1 file changed, 214 insertions(+), 33 deletions(-) diff --git a/test/secrets/passbolt_adapter.rb b/test/secrets/passbolt_adapter.rb index 1bb7c3b4..4a379d2a 100644 --- a/test/secrets/passbolt_adapter.rb +++ b/test/secrets/passbolt_adapter.rb @@ -68,10 +68,20 @@ class PassboltAdapterTest < SecretAdapterTestCase stub_ticks .with("passbolt list folders --filter 'Name == \"my-project\"' --json") - .returns(folder_my_project_json) + .returns(<<~JSON) + [ + { + "id": "dcbe0e39-42d8-42db-9637-8256b9f2f8e3", + "folder_parent_id": "", + "name": "my-project", + "created_timestamp": "2025-02-21T19:52:50Z", + "modified_timestamp": "2025-02-21T19:52:50Z" + } + ] + JSON stub_ticks - .with("passbolt list resources --filter 'Name == \"SECRET1\" || Name == \"FSECRET1\" || Name == \"FSECRET2\"' --folder dcbe0e39-42d8-42db-9637-8256b9f2f8e3 --json") + .with("passbolt list resources --filter '(Name == \"SECRET1\" && FolderParentID == \"dcbe0e39-42d8-42db-9637-8256b9f2f8e3\") || (Name == \"FSECRET1\" && FolderParentID == \"dcbe0e39-42d8-42db-9637-8256b9f2f8e3\") || (Name == \"FSECRET2\" && FolderParentID == \"dcbe0e39-42d8-42db-9637-8256b9f2f8e3\")' --folder dcbe0e39-42d8-42db-9637-8256b9f2f8e3 --json") .returns(<<~JSON) [ { @@ -129,10 +139,20 @@ class PassboltAdapterTest < SecretAdapterTestCase stub_ticks .with("passbolt list folders --filter 'Name == \"my-project\"' --json") - .returns(folder_my_project_json) + .returns(<<~JSON) + [ + { + "id": "dcbe0e39-42d8-42db-9637-8256b9f2f8e3", + "folder_parent_id": "", + "name": "my-project", + "created_timestamp": "2025-02-21T19:52:50Z", + "modified_timestamp": "2025-02-21T19:52:50Z" + } + ] + JSON stub_ticks - .with("passbolt list resources --filter 'Name == \"SECRET1\" || Name == \"FSECRET1\" || Name == \"FSECRET2\"' --folder dcbe0e39-42d8-42db-9637-8256b9f2f8e3 --json") + .with("passbolt list resources --filter '(Name == \"SECRET1\" && FolderParentID == \"dcbe0e39-42d8-42db-9637-8256b9f2f8e3\") || (Name == \"FSECRET1\" && FolderParentID == \"dcbe0e39-42d8-42db-9637-8256b9f2f8e3\") || (Name == \"FSECRET2\" && FolderParentID == \"dcbe0e39-42d8-42db-9637-8256b9f2f8e3\")' --folder dcbe0e39-42d8-42db-9637-8256b9f2f8e3 --json") .returns(<<~JSON) [ { @@ -189,15 +209,28 @@ class PassboltAdapterTest < SecretAdapterTestCase stub_ticks.with("passbolt verify 2> /dev/null", succeed: true) stub_ticks - .with("passbolt list folders --filter 'Name == \"my-project\"' --json") - .returns(folder_my_project_json) + .with("passbolt list folders --filter 'Name == \"my-project\" || Name == \"other-project\"' --json") + .returns(<<~JSON) + [ + { + "id": "dcbe0e39-42d8-42db-9637-8256b9f2f8e3", + "folder_parent_id": "", + "name": "my-project", + "created_timestamp": "2025-02-21T19:52:50Z", + "modified_timestamp": "2025-02-21T19:52:50Z" + }, + { + "id": "14e11dd8-b279-4689-8bd9-fa33ebb527da", + "folder_parent_id": "", + "name": "other-project", + "created_timestamp": "2025-02-21T20:00:29Z", + "modified_timestamp": "2025-02-21T20:00:29Z" + } + ] + JSON stub_ticks - .with("passbolt list folders --filter 'Name == \"other-project\"' --json") - .returns(folder_other_project_json) - - stub_ticks - .with("passbolt list resources --filter 'Name == \"SECRET1\" || Name == \"FSECRET1\" || Name == \"FSECRET2\"' --folder dcbe0e39-42d8-42db-9637-8256b9f2f8e3 --folder 14e11dd8-b279-4689-8bd9-fa33ebb527da --json") + .with("passbolt list resources --filter '(Name == \"SECRET1\" && FolderParentID == \"dcbe0e39-42d8-42db-9637-8256b9f2f8e3\") || (Name == \"FSECRET1\" && FolderParentID == \"dcbe0e39-42d8-42db-9637-8256b9f2f8e3\") || (Name == \"FSECRET2\" && FolderParentID == \"14e11dd8-b279-4689-8bd9-fa33ebb527da\")' --folder dcbe0e39-42d8-42db-9637-8256b9f2f8e3 --folder 14e11dd8-b279-4689-8bd9-fa33ebb527da --json") .returns(<<~JSON) [ { @@ -249,6 +282,176 @@ class PassboltAdapterTest < SecretAdapterTestCase assert_equal expected_json, json end + test "fetch from nested folder" do + stub_ticks_with("passbolt --version 2> /dev/null", succeed: true) + stub_ticks.with("passbolt verify 2> /dev/null", succeed: true) + + stub_ticks + .with("passbolt list folders --filter 'Name == \"my-project\"' --json") + .returns(<<~JSON) + [ + { + "id": "dcbe0e39-42d8-42db-9637-8256b9f2f8e3", + "folder_parent_id": "", + "name": "my-project", + "created_timestamp": "2025-02-21T19:52:50Z", + "modified_timestamp": "2025-02-21T19:52:50Z" + } + ] + JSON + + stub_ticks + .with("passbolt list folders --filter 'Name == \"subfolder\" && FolderParentID == \"dcbe0e39-42d8-42db-9637-8256b9f2f8e3\"' --json") + .returns(<<~JSON) + [ + { + "id": "6a3f21fc-aa40-4ba9-852c-7477fdd0310d", + "folder_parent_id": "dcbe0e39-42d8-42db-9637-8256b9f2f8e3", + "name": "subfolder", + "created_timestamp": "2025-02-21T19:52:50Z", + "modified_timestamp": "2025-02-21T19:52:50Z" + } + ] + JSON + + stub_ticks + .with("passbolt list resources --filter '(Name == \"SECRET1\" && FolderParentID == \"6a3f21fc-aa40-4ba9-852c-7477fdd0310d\") || (Name == \"FSECRET1\" && FolderParentID == \"6a3f21fc-aa40-4ba9-852c-7477fdd0310d\") || (Name == \"FSECRET2\" && FolderParentID == \"6a3f21fc-aa40-4ba9-852c-7477fdd0310d\")' --folder dcbe0e39-42d8-42db-9637-8256b9f2f8e3 --folder 6a3f21fc-aa40-4ba9-852c-7477fdd0310d --json") + .returns(<<~JSON) + [ + { + "id": "4c116996-f6d0-4342-9572-0d676f75b3ac", + "folder_parent_id": "6a3f21fc-aa40-4ba9-852c-7477fdd0310d", + "name": "FSECRET1", + "username": "", + "uri": "", + "password": "fsecret1", + "description": "", + "created_timestamp": "2025-02-21T06:04:29Z", + "modified_timestamp": "2025-02-21T06:04:29Z" + }, + { + "id": "62949b26-4957-43fe-9523-294d66861499", + "folder_parent_id": "6a3f21fc-aa40-4ba9-852c-7477fdd0310d", + "name": "FSECRET2", + "username": "", + "uri": "", + "password": "fsecret2", + "description": "", + "created_timestamp": "2025-02-21T06:04:34Z", + "modified_timestamp": "2025-02-21T06:04:34Z" + }, + { + "id": "dd32963c-0db5-4303-a6fc-22c5229dabef", + "folder_parent_id": "6a3f21fc-aa40-4ba9-852c-7477fdd0310d", + "name": "SECRET1", + "username": "", + "uri": "", + "password": "secret1", + "description": "", + "created_timestamp": "2025-02-21T06:04:23Z", + "modified_timestamp": "2025-02-21T06:04:23Z" + } + ] + JSON + + json = JSON.parse( + shellunescape run_command("fetch", "--from", "my-project/subfolder", "SECRET1", "FSECRET1", "FSECRET2") + ) + + expected_json = { + "SECRET1"=>"secret1", + "FSECRET1"=>"fsecret1", + "FSECRET2"=>"fsecret2" + } + + assert_equal expected_json, json + end + + test "fetch from nested folder in secret" do + stub_ticks_with("passbolt --version 2> /dev/null", succeed: true) + stub_ticks.with("passbolt verify 2> /dev/null", succeed: true) + + stub_ticks + .with("passbolt list folders --filter 'Name == \"my-project\"' --json") + .returns(<<~JSON) + [ + { + "id": "dcbe0e39-42d8-42db-9637-8256b9f2f8e3", + "folder_parent_id": "", + "name": "my-project", + "created_timestamp": "2025-02-21T19:52:50Z", + "modified_timestamp": "2025-02-21T19:52:50Z" + } + ] + JSON + + stub_ticks + .with("passbolt list folders --filter 'Name == \"subfolder\" && FolderParentID == \"dcbe0e39-42d8-42db-9637-8256b9f2f8e3\"' --json") + .returns(<<~JSON) + [ + { + "id": "6a3f21fc-aa40-4ba9-852c-7477fdd0310d", + "folder_parent_id": "dcbe0e39-42d8-42db-9637-8256b9f2f8e3", + "name": "subfolder", + "created_timestamp": "2025-02-21T19:52:50Z", + "modified_timestamp": "2025-02-21T19:52:50Z" + } + ] + JSON + + stub_ticks + .with("passbolt list resources --filter '(Name == \"SECRET1\" && FolderParentID == \"6a3f21fc-aa40-4ba9-852c-7477fdd0310d\") || (Name == \"FSECRET1\" && FolderParentID == \"6a3f21fc-aa40-4ba9-852c-7477fdd0310d\") || (Name == \"FSECRET2\" && FolderParentID == \"6a3f21fc-aa40-4ba9-852c-7477fdd0310d\")' --folder dcbe0e39-42d8-42db-9637-8256b9f2f8e3 --folder 6a3f21fc-aa40-4ba9-852c-7477fdd0310d --json") + .returns(<<~JSON) + [ + { + "id": "4c116996-f6d0-4342-9572-0d676f75b3ac", + "folder_parent_id": "6a3f21fc-aa40-4ba9-852c-7477fdd0310d", + "name": "FSECRET1", + "username": "", + "uri": "", + "password": "fsecret1", + "description": "", + "created_timestamp": "2025-02-21T06:04:29Z", + "modified_timestamp": "2025-02-21T06:04:29Z" + }, + { + "id": "62949b26-4957-43fe-9523-294d66861499", + "folder_parent_id": "6a3f21fc-aa40-4ba9-852c-7477fdd0310d", + "name": "FSECRET2", + "username": "", + "uri": "", + "password": "fsecret2", + "description": "", + "created_timestamp": "2025-02-21T06:04:34Z", + "modified_timestamp": "2025-02-21T06:04:34Z" + }, + { + "id": "dd32963c-0db5-4303-a6fc-22c5229dabef", + "folder_parent_id": "6a3f21fc-aa40-4ba9-852c-7477fdd0310d", + "name": "SECRET1", + "username": "", + "uri": "", + "password": "secret1", + "description": "", + "created_timestamp": "2025-02-21T06:04:23Z", + "modified_timestamp": "2025-02-21T06:04:23Z" + } + ] + JSON + + json = JSON.parse( + shellunescape run_command("fetch", "my-project/subfolder/SECRET1", "my-project/subfolder/FSECRET1", "my-project/subfolder/FSECRET2") + ) + + expected_json = { + "SECRET1"=>"secret1", + "FSECRET1"=>"fsecret1", + "FSECRET2"=>"fsecret2" + } + + assert_equal expected_json, json + end + test "fetch without CLI installed" do stub_ticks_with("passbolt --version 2> /dev/null", succeed: false) @@ -268,26 +471,4 @@ class PassboltAdapterTest < SecretAdapterTestCase "--adapter", "passbolt" ] end end - - def folder_my_project_json - <<~JSON - [ - { - "id": "dcbe0e39-42d8-42db-9637-8256b9f2f8e3", - "name": "my-project" - } - ] - JSON - end - - def folder_other_project_json - <<~JSON - [ - { - "id": "14e11dd8-b279-4689-8bd9-fa33ebb527da", - "name": "other-project" - } - ] - JSON - end end \ No newline at end of file From aa12dc1d1205a47d31002cd7644e05546e57ca76 Mon Sep 17 00:00:00 2001 From: acidtib Date: Fri, 21 Feb 2025 17:52:17 -0700 Subject: [PATCH 5/5] remove unnecessary blank lines --- lib/kamal/secrets/adapters/passbolt.rb | 20 ++++++++++---------- test/secrets/passbolt_adapter.rb | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/kamal/secrets/adapters/passbolt.rb b/lib/kamal/secrets/adapters/passbolt.rb index 90e9ee27..9b9e2204 100644 --- a/lib/kamal/secrets/adapters/passbolt.rb +++ b/lib/kamal/secrets/adapters/passbolt.rb @@ -22,15 +22,15 @@ class Kamal::Secrets::Adapters::Passbolt < Kamal::Secrets::Adapters::Base secrets.each do |secret| parts = secret.split("/") secret_name = parts.last - + if parts.size > 1 # get the folder path without the secret name folder_path = parts[0..-2] - + # find the most nested folder for this path current_folder = nil current_path = [] - + folder_path.each do |folder_name| current_path << folder_name matching_folders = folders.select { |f| get_folder_path(f, folders) == current_path.join("/") } @@ -55,7 +55,7 @@ class Kamal::Secrets::Adapters::Passbolt < Kamal::Secrets::Adapters::Base missing_secrets = secret_names - found_names raise RuntimeError, "Could not find the following secrets in Passbolt: #{missing_secrets.join(", ")}" if missing_secrets.any? - items.to_h { |item| [item["name"], item["password"]] } + items.to_h { |item| [ item["name"], item["password"] ] } end def secrets_get_folders(secrets) @@ -68,7 +68,7 @@ class Kamal::Secrets::Adapters::Passbolt < Kamal::Secrets::Adapters::Base return [] if folder_paths.empty? all_folders = [] - + # first get all top-level folders parent_folders = folder_paths.map(&:first).uniq filter_condition = "--filter '#{parent_folders.map { |name| "Name == #{name.shellescape.inspect}" }.join(" || ")}'" @@ -81,7 +81,7 @@ class Kamal::Secrets::Adapters::Passbolt < Kamal::Secrets::Adapters::Base # get nested folders for each parent folder_paths.each do |path| next if path.size <= 1 # skip non-nested folders - + parent = path[0] parent_folder = parent_folder_items.find { |f| f["name"] == parent } next unless parent_folder @@ -95,7 +95,7 @@ class Kamal::Secrets::Adapters::Passbolt < Kamal::Secrets::Adapters::Base nested_folders = JSON.parse(fetch_nested) break if nested_folders.empty? - + all_folders.concat(nested_folders) current_parent = nested_folders.first end @@ -112,10 +112,10 @@ class Kamal::Secrets::Adapters::Passbolt < Kamal::Secrets::Adapters::Base def get_folder_path(folder, all_folders, path = []) path.unshift(folder["name"]) return path.join("/") if folder["folder_parent_id"].to_s.empty? - + parent = all_folders.find { |f| f["id"] == folder["folder_parent_id"] } return path.join("/") unless parent - + get_folder_path(parent, all_folders, path) end @@ -127,4 +127,4 @@ class Kamal::Secrets::Adapters::Passbolt < Kamal::Secrets::Adapters::Base `passbolt --version 2> /dev/null` $?.success? end -end \ No newline at end of file +end diff --git a/test/secrets/passbolt_adapter.rb b/test/secrets/passbolt_adapter.rb index 4a379d2a..bcfed285 100644 --- a/test/secrets/passbolt_adapter.rb +++ b/test/secrets/passbolt_adapter.rb @@ -471,4 +471,4 @@ class PassboltAdapterTest < SecretAdapterTestCase "--adapter", "passbolt" ] end end -end \ No newline at end of file +end