From a726a86a17dda1f41f7c0d857096dd73fa529e3a Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 29 Aug 2024 15:29:39 +0100 Subject: [PATCH] Add lastpass, bitwarden adapters --- lib/kamal/secrets/adapters/base.rb | 24 ++++++++++++ lib/kamal/secrets/adapters/bitwarden.rb | 52 +++++++++++++++++++++++++ lib/kamal/secrets/adapters/last_pass.rb | 29 ++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 lib/kamal/secrets/adapters/base.rb create mode 100644 lib/kamal/secrets/adapters/bitwarden.rb create mode 100644 lib/kamal/secrets/adapters/last_pass.rb diff --git a/lib/kamal/secrets/adapters/base.rb b/lib/kamal/secrets/adapters/base.rb new file mode 100644 index 00000000..9432913d --- /dev/null +++ b/lib/kamal/secrets/adapters/base.rb @@ -0,0 +1,24 @@ +class Kamal::Secrets::Adapters::Base + delegate :optionize, to: Kamal::Utils + + def fetch(secrets, account:, location: nil) + session = login(account) + full_secrets = secrets.map { |secret| [ location, secret ].compact.join("/") } + fetch_from_vault(full_secrets, account: account, session: session) + rescue => e + $stderr.puts " \e[31mERROR (#{e.class}): #{e.message}\e[0m" + $stderr.puts e.backtrace if ENV["VERBOSE"] + + Process.kill("INT", Process.ppid) if ENV["KAMAL_SECRETS_KILL_PARENT"] + exit 1 + end + + private + def login(...) + raise NotImplementedError + end + + def fetch_from_vault(...) + raise NotImplementedError + end +end diff --git a/lib/kamal/secrets/adapters/bitwarden.rb b/lib/kamal/secrets/adapters/bitwarden.rb new file mode 100644 index 00000000..e48ce82b --- /dev/null +++ b/lib/kamal/secrets/adapters/bitwarden.rb @@ -0,0 +1,52 @@ +class Kamal::Secrets::Adapters::Bitwarden < Kamal::Secrets::Adapters::Base + private + def login(account) + status = run_command("status") + + if status["status"] == "unauthenticated" + run_command("login #{account}") + status = run_command("status") + end + + if status["status"] == "locked" + session = run_command("unlock --raw", raw: true) + status = run_command("status", session: session) + end + + raise RuntimeError, "Failed to login to and unlock Bitwarden" unless status["status"] == "unlocked" + + run_command("sync", raw: true) + raise RuntimeError, "Failed to sync Bitwarden" unless $?.success? + + session + end + + def fetch_from_vault(secrets, account:, session:) + {}.tap do |results| + secrets.each do |secret| + item, field = secret.split("/") + item = run_command("get item #{item}", session: session) + raise RuntimeError, "Could not read #{item} from Bitwarden" unless $?.success? + if field + item_field = item["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[secret] = value + results[field] = value + else + results[secret] = item["login"]["password"] + end + end + end + end + + def signedin?(account) + JSON.parse(`bw status`.strip)["status"] != "unauthenticated" + end + + def run_command(command, session: nil, raw: false) + full_command = [ *("BW_SESSION=#{session}" if session), "bw", command ].join(" ") + result = `#{full_command}`.strip + raw ? result : JSON.parse(result) + end +end diff --git a/lib/kamal/secrets/adapters/last_pass.rb b/lib/kamal/secrets/adapters/last_pass.rb new file mode 100644 index 00000000..984a684a --- /dev/null +++ b/lib/kamal/secrets/adapters/last_pass.rb @@ -0,0 +1,29 @@ +class Kamal::Secrets::Adapters::LastPass < Kamal::Secrets::Adapters::Base + private + def login(account) + unless loggedin?(account) + `lpass login #{account}` + raise RuntimeError, "Failed to login to 1Password" unless $?.success? + end + end + + def loggedin?(account) + `lpass status --color never`.strip == "Logged in as #{account}." + end + + def fetch_from_vault(secrets, account:, session:) + items = JSON.parse(`lpass show #{secrets.join(" ")} --json` + raise RuntimeError, "Could not read #{fields} from 1Password" unless $?.success? + + {}.tap do |results| + items.each do |item| + results[item["name"]] = item["password"] + results[item["fullname"]] = item["password"] + end + + if (missing_items = secrets - results.keys).any? + raise RuntimeError, "Could not find #{missing_items.join(", ")} in LassPass" + end + end + end +end