Merge pull request #1570 from nickcoyne/bws-secrets

Request Bitwarden Secrets Manager secrets as JSON
This commit is contained in:
Donal McBreen
2025-06-16 10:59:53 +01:00
committed by GitHub
2 changed files with 108 additions and 40 deletions

View File

@@ -6,8 +6,8 @@ class Kamal::Secrets::Adapters::BitwardenSecretsManager < Kamal::Secrets::Adapte
private private
LIST_ALL_SELECTOR = "all" LIST_ALL_SELECTOR = "all"
LIST_ALL_FROM_PROJECT_SUFFIX = "/all" LIST_ALL_FROM_PROJECT_SUFFIX = "/all"
LIST_COMMAND = "secret list -o env" LIST_COMMAND = "secret list"
GET_COMMAND = "secret get -o env" GET_COMMAND = "secret get"
def fetch_secrets(secrets, from:, account:, session:) def fetch_secrets(secrets, from:, account:, session:)
raise RuntimeError, "You must specify what to retrieve from Bitwarden Secrets Manager" if secrets.length == 0 raise RuntimeError, "You must specify what to retrieve from Bitwarden Secrets Manager" if secrets.length == 0
@@ -18,17 +18,17 @@ class Kamal::Secrets::Adapters::BitwardenSecretsManager < Kamal::Secrets::Adapte
{}.tap do |results| {}.tap do |results|
if command.nil? if command.nil?
secrets.each do |secret_uuid| secrets.each do |secret_uuid|
secret = run_command("#{GET_COMMAND} #{secret_uuid.shellescape}") item_json = run_command("#{GET_COMMAND} #{secret_uuid.shellescape}")
raise RuntimeError, "Could not read #{secret_uuid} from Bitwarden Secrets Manager" unless $?.success? raise RuntimeError, "Could not read #{secret_uuid} from Bitwarden Secrets Manager" unless $?.success?
key, value = parse_secret(secret) item_json = JSON.parse(item_json)
results[key] = value results[item_json["key"]] = item_json["value"]
end end
else else
secrets = run_command(command) items_json = run_command(command)
raise RuntimeError, "Could not read secrets from Bitwarden Secrets Manager" unless $?.success? raise RuntimeError, "Could not read secrets from Bitwarden Secrets Manager" unless $?.success?
secrets.split("\n").each do |secret|
key, value = parse_secret(secret) JSON.parse(items_json).each do |item_json|
results[key] = value results[item_json["key"]] = item_json["value"]
end end
end end
end end
@@ -45,12 +45,6 @@ class Kamal::Secrets::Adapters::BitwardenSecretsManager < Kamal::Secrets::Adapte
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) def run_command(command, session: nil)
full_command = [ "bws", command ].join(" ") full_command = [ "bws", command ].join(" ")
`#{full_command}` `#{full_command}`

View File

@@ -15,57 +15,111 @@ class BitwardenSecretsManagerAdapterTest < SecretAdapterTestCase
stub_ticks.with("bws --version 2> /dev/null") stub_ticks.with("bws --version 2> /dev/null")
stub_login stub_login
stub_ticks stub_ticks
.with("bws secret list -o env") .with("bws secret list")
.returns("KAMAL_REGISTRY_PASSWORD=\"some_password\"\nMY_OTHER_SECRET=\"my=weird\"secret\"") .returns(<<~JSON)
[
{
"key": "KAMAL_REGISTRY_PASSWORD",
"value": "some_password"
},
{
"key": "MY_OTHER_SECRET",
"value": "my=wierd\\"secret"
}
]
JSON
expected = '{"KAMAL_REGISTRY_PASSWORD":"some_password","MY_OTHER_SECRET":"my\=weird\"secret"}' json = JSON.parse(shellunescape(run_command("fetch", "all")))
actual = shellunescape(run_command("fetch", "all"))
assert_equal expected, actual expected_json = {
"KAMAL_REGISTRY_PASSWORD"=>"some_password",
"MY_OTHER_SECRET"=>"my=wierd\"secret"
}
assert_equal expected_json, json
end end
test "fetch all with from" do test "fetch all with from" do
stub_ticks.with("bws --version 2> /dev/null") stub_ticks.with("bws --version 2> /dev/null")
stub_login stub_login
stub_ticks stub_ticks
.with("bws secret list -o env 82aeb5bd-6958-4a89-8197-eacab758acce") .with("bws secret list 82aeb5bd-6958-4a89-8197-eacab758acce")
.returns("KAMAL_REGISTRY_PASSWORD=\"some_password\"\nMY_OTHER_SECRET=\"my=weird\"secret\"") .returns(<<~JSON)
[
{
"key": "KAMAL_REGISTRY_PASSWORD",
"value": "some_password"
},
{
"key": "MY_OTHER_SECRET",
"value": "my=wierd\\"secret"
}
]
JSON
expected = '{"KAMAL_REGISTRY_PASSWORD":"some_password","MY_OTHER_SECRET":"my\=weird\"secret"}' json = JSON.parse(shellunescape(run_command("fetch", "all", "--from", "82aeb5bd-6958-4a89-8197-eacab758acce")))
actual = shellunescape(run_command("fetch", "all", "--from", "82aeb5bd-6958-4a89-8197-eacab758acce"))
assert_equal expected, actual expected_json = {
"KAMAL_REGISTRY_PASSWORD"=>"some_password",
"MY_OTHER_SECRET"=>"my=wierd\"secret"
}
assert_equal expected_json, json
end end
test "fetch item" do test "fetch item" do
stub_ticks.with("bws --version 2> /dev/null") stub_ticks.with("bws --version 2> /dev/null")
stub_login stub_login
stub_ticks stub_ticks
.with("bws secret get -o env 82aeb5bd-6958-4a89-8197-eacab758acce") .with("bws secret get 82aeb5bd-6958-4a89-8197-eacab758acce")
.returns("KAMAL_REGISTRY_PASSWORD=\"some_password\"") .returns(<<~JSON)
{
"key": "KAMAL_REGISTRY_PASSWORD",
"value": "some_password"
}
JSON
expected = '{"KAMAL_REGISTRY_PASSWORD":"some_password"}' json = JSON.parse(shellunescape(run_command("fetch", "82aeb5bd-6958-4a89-8197-eacab758acce")))
actual = shellunescape(run_command("fetch", "82aeb5bd-6958-4a89-8197-eacab758acce")) expected_json = {
assert_equal expected, actual "KAMAL_REGISTRY_PASSWORD"=>"some_password"
}
assert_equal expected_json, json
end end
test "fetch with multiple items" do test "fetch with multiple items" do
stub_ticks.with("bws --version 2> /dev/null") stub_ticks.with("bws --version 2> /dev/null")
stub_login stub_login
stub_ticks stub_ticks
.with("bws secret get -o env 82aeb5bd-6958-4a89-8197-eacab758acce") .with("bws secret get 82aeb5bd-6958-4a89-8197-eacab758acce")
.returns("KAMAL_REGISTRY_PASSWORD=\"some_password\"") .returns(<<~JSON)
{
"key": "KAMAL_REGISTRY_PASSWORD",
"value": "some_password"
}
JSON
stub_ticks stub_ticks
.with("bws secret get -o env 6f8cdf27-de2b-4c77-a35d-07df8050e332") .with("bws secret get 6f8cdf27-de2b-4c77-a35d-07df8050e332")
.returns("MY_OTHER_SECRET=\"my=weird\"secret\"") .returns(<<~JSON)
{
"key": "MY_OTHER_SECRET",
"value": "my=wierd\\"secret"
}
JSON
expected = '{"KAMAL_REGISTRY_PASSWORD":"some_password","MY_OTHER_SECRET":"my\=weird\"secret"}' json = JSON.parse(shellunescape(run_command("fetch", "82aeb5bd-6958-4a89-8197-eacab758acce", "6f8cdf27-de2b-4c77-a35d-07df8050e332")))
actual = shellunescape(run_command("fetch", "82aeb5bd-6958-4a89-8197-eacab758acce", "6f8cdf27-de2b-4c77-a35d-07df8050e332")) expected_json = {
assert_equal expected, actual "KAMAL_REGISTRY_PASSWORD"=>"some_password",
"MY_OTHER_SECRET"=>"my=wierd\"secret"
}
assert_equal expected_json, json
end end
test "fetch all empty" do test "fetch all empty" do
stub_ticks.with("bws --version 2> /dev/null") stub_ticks.with("bws --version 2> /dev/null")
stub_login stub_login
stub_ticks_with("bws secret list -o env", succeed: false).returns("Error:\n0: Received error message from server") stub_ticks_with("bws secret list", succeed: false).returns("Error:\n0: Received error message from server")
error = assert_raises RuntimeError do error = assert_raises RuntimeError do
(shellunescape(run_command("fetch", "all"))) (shellunescape(run_command("fetch", "all")))
@@ -76,8 +130,8 @@ class BitwardenSecretsManagerAdapterTest < SecretAdapterTestCase
test "fetch nonexistent item" do test "fetch nonexistent item" do
stub_ticks.with("bws --version 2> /dev/null") stub_ticks.with("bws --version 2> /dev/null")
stub_login stub_login
stub_ticks_with("bws secret get -o env 82aeb5bd-6958-4a89-8197-eacab758acce", succeed: false) stub_ticks_with("bws secret get 82aeb5bd-6958-4a89-8197-eacab758acce", succeed: false)
.returns("ERROR (RuntimeError): Could not read 82aeb5bd-6958-4a89-8197-eacab758acce from Bitwarden Secrets Manager") .returns("Error:\n0: Received error message from server")
error = assert_raises RuntimeError do error = assert_raises RuntimeError do
(shellunescape(run_command("fetch", "82aeb5bd-6958-4a89-8197-eacab758acce"))) (shellunescape(run_command("fetch", "82aeb5bd-6958-4a89-8197-eacab758acce")))
@@ -85,6 +139,26 @@ class BitwardenSecretsManagerAdapterTest < SecretAdapterTestCase
assert_equal("Could not read 82aeb5bd-6958-4a89-8197-eacab758acce from Bitwarden Secrets Manager", error.message) assert_equal("Could not read 82aeb5bd-6958-4a89-8197-eacab758acce from Bitwarden Secrets Manager", error.message)
end end
test "fetch item with linebreak in value" do
stub_ticks.with("bws --version 2> /dev/null")
stub_login
stub_ticks
.with("bws secret get 82aeb5bd-6958-4a89-8197-eacab758acce")
.returns(<<~JSON)
{
"key": "SSH_PRIVATE_KEY",
"value": "some_key\\nwith_linebreak"
}
JSON
json = JSON.parse(shellunescape(run_command("fetch", "82aeb5bd-6958-4a89-8197-eacab758acce")))
expected_json = {
"SSH_PRIVATE_KEY"=>"some_key\nwith_linebreak"
}
assert_equal expected_json, json
end
test "fetch with no access token" do test "fetch with no access token" do
stub_ticks.with("bws --version 2> /dev/null") stub_ticks.with("bws --version 2> /dev/null")
stub_ticks_with("bws project list", succeed: false) stub_ticks_with("bws project list", succeed: false)