feat: add Bitwarden Secrets Manager adapter
This commit is contained in:
@@ -3,6 +3,7 @@ module Kamal::Secrets::Adapters
|
|||||||
def self.lookup(name)
|
def self.lookup(name)
|
||||||
name = "one_password" if name.downcase == "1password"
|
name = "one_password" if name.downcase == "1password"
|
||||||
name = "last_pass" if name.downcase == "lastpass"
|
name = "last_pass" if name.downcase == "lastpass"
|
||||||
|
name = "bitwarden_secrets_manager" if name.downcase == "bitwarden-sm"
|
||||||
adapter_class(name)
|
adapter_class(name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
67
lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb
Normal file
67
lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
class Kamal::Secrets::Adapters::BitwardenSecretsManager < Kamal::Secrets::Adapters::Base
|
||||||
|
def requires_account?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
LIST_ALL_SELECTOR = "all"
|
||||||
|
LIST_ALL_FROM_PROJECT_SUFFIX = "/all"
|
||||||
|
LIST_COMMAND = "secret list -o env"
|
||||||
|
GET_COMMAND = "secret get -o env"
|
||||||
|
|
||||||
|
def fetch_secrets(secrets, account:, session:)
|
||||||
|
raise RuntimeError, "You must specify what to retrieve from Bitwarden Secrets Manager" if secrets.length == 0
|
||||||
|
|
||||||
|
if secrets.length == 1
|
||||||
|
if secrets[0] == LIST_ALL_SELECTOR
|
||||||
|
command = LIST_COMMAND
|
||||||
|
elsif secrets[0].end_with?(LIST_ALL_FROM_PROJECT_SUFFIX)
|
||||||
|
project = secrets[0].split(LIST_ALL_FROM_PROJECT_SUFFIX).first
|
||||||
|
command = "#{LIST_COMMAND} #{project}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
{}.tap do |results|
|
||||||
|
if command.nil?
|
||||||
|
secrets.each do |secret_uuid|
|
||||||
|
secret = run_command("#{GET_COMMAND} #{secret_uuid}")
|
||||||
|
raise RuntimeError, "Could not read #{secret_uuid} from Bitwarden Secrets Manager" unless $?.success?
|
||||||
|
key, value = parse_secret(secret)
|
||||||
|
results[key] = value
|
||||||
|
end
|
||||||
|
else
|
||||||
|
secrets = run_command(command)
|
||||||
|
raise RuntimeError, "Could not read secrets from Bitwarden Secrets Manager" unless $?.success?
|
||||||
|
secrets.split("\n").each do |secret|
|
||||||
|
key, value = parse_secret(secret)
|
||||||
|
results[key] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_secret(secret)
|
||||||
|
key, value = secret.split("=", 2)
|
||||||
|
value = value.gsub(/^"|"$/, "")
|
||||||
|
[ key, value ]
|
||||||
|
end
|
||||||
|
|
||||||
|
def run_command(command, session: nil)
|
||||||
|
full_command = [ "bws", command ].join(" ")
|
||||||
|
`#{full_command}`
|
||||||
|
end
|
||||||
|
|
||||||
|
def login(account)
|
||||||
|
run_command("run 'echo OK'")
|
||||||
|
raise RuntimeError, "Could not authenticate to Bitwarden Secrets Manager. Did you set a valid access token?" unless $?.success?
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_dependencies!
|
||||||
|
raise RuntimeError, "Bitwarden Secrets Manager CLI is not installed" unless cli_installed?
|
||||||
|
end
|
||||||
|
|
||||||
|
def cli_installed?
|
||||||
|
`bws --version 2> /dev/null`
|
||||||
|
$?.success?
|
||||||
|
end
|
||||||
|
end
|
||||||
119
test/secrets/bitwarden_secrets_manager_adapter_test.rb
Normal file
119
test/secrets/bitwarden_secrets_manager_adapter_test.rb
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class BitwardenSecretsManagerAdapterTest < SecretAdapterTestCase
|
||||||
|
test "fetch with no parameters" do
|
||||||
|
stub_ticks.with("bws --version 2> /dev/null")
|
||||||
|
stub_login
|
||||||
|
|
||||||
|
error = assert_raises RuntimeError do
|
||||||
|
(shellunescape(run_command("fetch")))
|
||||||
|
end
|
||||||
|
assert_equal("You must specify what to retrieve from Bitwarden Secrets Manager", error.message)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "fetch all" do
|
||||||
|
stub_ticks.with("bws --version 2> /dev/null")
|
||||||
|
stub_login
|
||||||
|
stub_ticks
|
||||||
|
.with("bws secret list -o env")
|
||||||
|
.returns("KAMAL_REGISTRY_PASSWORD=\"some_password\"\nMY_OTHER_SECRET=\"my=weird\"secret\"")
|
||||||
|
|
||||||
|
expected = '{"KAMAL_REGISTRY_PASSWORD":"some_password","MY_OTHER_SECRET":"my\=weird\"secret"}'
|
||||||
|
actual = shellunescape(run_command("fetch", "all"))
|
||||||
|
assert_equal expected, actual
|
||||||
|
end
|
||||||
|
|
||||||
|
test "fetch all with from" do
|
||||||
|
stub_ticks.with("bws --version 2> /dev/null")
|
||||||
|
stub_login
|
||||||
|
stub_ticks
|
||||||
|
.with("bws secret list -o env 82aeb5bd-6958-4a89-8197-eacab758acce")
|
||||||
|
.returns("KAMAL_REGISTRY_PASSWORD=\"some_password\"\nMY_OTHER_SECRET=\"my=weird\"secret\"")
|
||||||
|
|
||||||
|
expected = '{"KAMAL_REGISTRY_PASSWORD":"some_password","MY_OTHER_SECRET":"my\=weird\"secret"}'
|
||||||
|
actual = shellunescape(run_command("fetch", "all", "--from", "82aeb5bd-6958-4a89-8197-eacab758acce"))
|
||||||
|
assert_equal expected, actual
|
||||||
|
end
|
||||||
|
|
||||||
|
test "fetch item" do
|
||||||
|
stub_ticks.with("bws --version 2> /dev/null")
|
||||||
|
stub_login
|
||||||
|
stub_ticks
|
||||||
|
.with("bws secret get -o env 82aeb5bd-6958-4a89-8197-eacab758acce")
|
||||||
|
.returns("KAMAL_REGISTRY_PASSWORD=\"some_password\"")
|
||||||
|
|
||||||
|
expected = '{"KAMAL_REGISTRY_PASSWORD":"some_password"}'
|
||||||
|
actual = shellunescape(run_command("fetch", "82aeb5bd-6958-4a89-8197-eacab758acce"))
|
||||||
|
assert_equal expected, actual
|
||||||
|
end
|
||||||
|
|
||||||
|
test "fetch with multiple items" do
|
||||||
|
stub_ticks.with("bws --version 2> /dev/null")
|
||||||
|
stub_login
|
||||||
|
stub_ticks
|
||||||
|
.with("bws secret get -o env 82aeb5bd-6958-4a89-8197-eacab758acce")
|
||||||
|
.returns("KAMAL_REGISTRY_PASSWORD=\"some_password\"")
|
||||||
|
stub_ticks
|
||||||
|
.with("bws secret get -o env 6f8cdf27-de2b-4c77-a35d-07df8050e332")
|
||||||
|
.returns("MY_OTHER_SECRET=\"my=weird\"secret\"")
|
||||||
|
|
||||||
|
expected = '{"KAMAL_REGISTRY_PASSWORD":"some_password","MY_OTHER_SECRET":"my\=weird\"secret"}'
|
||||||
|
actual = shellunescape(run_command("fetch", "82aeb5bd-6958-4a89-8197-eacab758acce", "6f8cdf27-de2b-4c77-a35d-07df8050e332"))
|
||||||
|
assert_equal expected, actual
|
||||||
|
end
|
||||||
|
|
||||||
|
test "fetch all empty" do
|
||||||
|
stub_ticks.with("bws --version 2> /dev/null")
|
||||||
|
stub_login
|
||||||
|
stub_ticks_with("bws secret list -o env", succeed: false).returns("Error:\n0: Received error message from server")
|
||||||
|
|
||||||
|
error = assert_raises RuntimeError do
|
||||||
|
(shellunescape(run_command("fetch", "all")))
|
||||||
|
end
|
||||||
|
assert_equal("Could not read secrets from Bitwarden Secrets Manager", error.message)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "fetch nonexistent item" do
|
||||||
|
stub_ticks.with("bws --version 2> /dev/null")
|
||||||
|
stub_login
|
||||||
|
stub_ticks_with("bws secret get -o env 82aeb5bd-6958-4a89-8197-eacab758acce", succeed: false)
|
||||||
|
.returns("ERROR (RuntimeError): Could not read 82aeb5bd-6958-4a89-8197-eacab758acce from Bitwarden Secrets Manager")
|
||||||
|
|
||||||
|
error = assert_raises RuntimeError do
|
||||||
|
(shellunescape(run_command("fetch", "82aeb5bd-6958-4a89-8197-eacab758acce")))
|
||||||
|
end
|
||||||
|
assert_equal("Could not read 82aeb5bd-6958-4a89-8197-eacab758acce from Bitwarden Secrets Manager", error.message)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "fetch with no access token" do
|
||||||
|
stub_ticks.with("bws --version 2> /dev/null")
|
||||||
|
stub_ticks_with("bws run 'echo OK'", succeed: false)
|
||||||
|
|
||||||
|
error = assert_raises RuntimeError do
|
||||||
|
(shellunescape(run_command("fetch", "all")))
|
||||||
|
end
|
||||||
|
assert_equal("Could not authenticate to Bitwarden Secrets Manager. Did you set a valid access token?", error.message)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "fetch without CLI installed" do
|
||||||
|
stub_ticks_with("bws --version 2> /dev/null", succeed: false)
|
||||||
|
|
||||||
|
error = assert_raises RuntimeError do
|
||||||
|
shellunescape(run_command("fetch"))
|
||||||
|
end
|
||||||
|
assert_equal "Bitwarden Secrets Manager CLI is not installed", error.message
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def stub_login
|
||||||
|
stub_ticks.with("bws run 'echo OK'").returns("OK")
|
||||||
|
end
|
||||||
|
|
||||||
|
def run_command(*command)
|
||||||
|
stdouted do
|
||||||
|
Kamal::Cli::Secrets.start \
|
||||||
|
[ *command,
|
||||||
|
"--adapter", "bitwarden-sm" ]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user