Skip to content

Commit ea7e72d

Browse files
authored
Merge pull request #1186 from oandalib/bitwarden-secrets-manager
feat: add Bitwarden Secrets Manager adapter
2 parents 9035bd0 + aa9fe4c commit ea7e72d

File tree

3 files changed

+187
-0
lines changed

3 files changed

+187
-0
lines changed

lib/kamal/secrets/adapters.rb

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module Kamal::Secrets::Adapters
33
def self.lookup(name)
44
name = "one_password" if name.downcase == "1password"
55
name = "last_pass" if name.downcase == "lastpass"
6+
name = "bitwarden_secrets_manager" if name.downcase == "bitwarden-sm"
67
adapter_class(name)
78
end
89

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
class Kamal::Secrets::Adapters::BitwardenSecretsManager < Kamal::Secrets::Adapters::Base
2+
def requires_account?
3+
false
4+
end
5+
6+
private
7+
LIST_ALL_SELECTOR = "all"
8+
LIST_ALL_FROM_PROJECT_SUFFIX = "/all"
9+
LIST_COMMAND = "secret list -o env"
10+
GET_COMMAND = "secret get -o env"
11+
12+
def fetch_secrets(secrets, account:, session:)
13+
raise RuntimeError, "You must specify what to retrieve from Bitwarden Secrets Manager" if secrets.length == 0
14+
15+
if secrets.length == 1
16+
if secrets[0] == LIST_ALL_SELECTOR
17+
command = LIST_COMMAND
18+
elsif secrets[0].end_with?(LIST_ALL_FROM_PROJECT_SUFFIX)
19+
project = secrets[0].split(LIST_ALL_FROM_PROJECT_SUFFIX).first
20+
command = "#{LIST_COMMAND} #{project}"
21+
end
22+
end
23+
24+
{}.tap do |results|
25+
if command.nil?
26+
secrets.each do |secret_uuid|
27+
secret = run_command("#{GET_COMMAND} #{secret_uuid}")
28+
raise RuntimeError, "Could not read #{secret_uuid} from Bitwarden Secrets Manager" unless $?.success?
29+
key, value = parse_secret(secret)
30+
results[key] = value
31+
end
32+
else
33+
secrets = run_command(command)
34+
raise RuntimeError, "Could not read secrets from Bitwarden Secrets Manager" unless $?.success?
35+
secrets.split("\n").each do |secret|
36+
key, value = parse_secret(secret)
37+
results[key] = value
38+
end
39+
end
40+
end
41+
end
42+
43+
def parse_secret(secret)
44+
key, value = secret.split("=", 2)
45+
value = value.gsub(/^"|"$/, "")
46+
[ key, value ]
47+
end
48+
49+
def run_command(command, session: nil)
50+
full_command = [ "bws", command ].join(" ")
51+
`#{full_command}`
52+
end
53+
54+
def login(account)
55+
run_command("run 'echo OK'")
56+
raise RuntimeError, "Could not authenticate to Bitwarden Secrets Manager. Did you set a valid access token?" unless $?.success?
57+
end
58+
59+
def check_dependencies!
60+
raise RuntimeError, "Bitwarden Secrets Manager CLI is not installed" unless cli_installed?
61+
end
62+
63+
def cli_installed?
64+
`bws --version 2> /dev/null`
65+
$?.success?
66+
end
67+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
require "test_helper"
2+
3+
class BitwardenSecretsManagerAdapterTest < SecretAdapterTestCase
4+
test "fetch with no parameters" do
5+
stub_ticks.with("bws --version 2> /dev/null")
6+
stub_login
7+
8+
error = assert_raises RuntimeError do
9+
(shellunescape(run_command("fetch")))
10+
end
11+
assert_equal("You must specify what to retrieve from Bitwarden Secrets Manager", error.message)
12+
end
13+
14+
test "fetch all" do
15+
stub_ticks.with("bws --version 2> /dev/null")
16+
stub_login
17+
stub_ticks
18+
.with("bws secret list -o env")
19+
.returns("KAMAL_REGISTRY_PASSWORD=\"some_password\"\nMY_OTHER_SECRET=\"my=weird\"secret\"")
20+
21+
expected = '{"KAMAL_REGISTRY_PASSWORD":"some_password","MY_OTHER_SECRET":"my\=weird\"secret"}'
22+
actual = shellunescape(run_command("fetch", "all"))
23+
assert_equal expected, actual
24+
end
25+
26+
test "fetch all with from" do
27+
stub_ticks.with("bws --version 2> /dev/null")
28+
stub_login
29+
stub_ticks
30+
.with("bws secret list -o env 82aeb5bd-6958-4a89-8197-eacab758acce")
31+
.returns("KAMAL_REGISTRY_PASSWORD=\"some_password\"\nMY_OTHER_SECRET=\"my=weird\"secret\"")
32+
33+
expected = '{"KAMAL_REGISTRY_PASSWORD":"some_password","MY_OTHER_SECRET":"my\=weird\"secret"}'
34+
actual = shellunescape(run_command("fetch", "all", "--from", "82aeb5bd-6958-4a89-8197-eacab758acce"))
35+
assert_equal expected, actual
36+
end
37+
38+
test "fetch item" do
39+
stub_ticks.with("bws --version 2> /dev/null")
40+
stub_login
41+
stub_ticks
42+
.with("bws secret get -o env 82aeb5bd-6958-4a89-8197-eacab758acce")
43+
.returns("KAMAL_REGISTRY_PASSWORD=\"some_password\"")
44+
45+
expected = '{"KAMAL_REGISTRY_PASSWORD":"some_password"}'
46+
actual = shellunescape(run_command("fetch", "82aeb5bd-6958-4a89-8197-eacab758acce"))
47+
assert_equal expected, actual
48+
end
49+
50+
test "fetch with multiple items" do
51+
stub_ticks.with("bws --version 2> /dev/null")
52+
stub_login
53+
stub_ticks
54+
.with("bws secret get -o env 82aeb5bd-6958-4a89-8197-eacab758acce")
55+
.returns("KAMAL_REGISTRY_PASSWORD=\"some_password\"")
56+
stub_ticks
57+
.with("bws secret get -o env 6f8cdf27-de2b-4c77-a35d-07df8050e332")
58+
.returns("MY_OTHER_SECRET=\"my=weird\"secret\"")
59+
60+
expected = '{"KAMAL_REGISTRY_PASSWORD":"some_password","MY_OTHER_SECRET":"my\=weird\"secret"}'
61+
actual = shellunescape(run_command("fetch", "82aeb5bd-6958-4a89-8197-eacab758acce", "6f8cdf27-de2b-4c77-a35d-07df8050e332"))
62+
assert_equal expected, actual
63+
end
64+
65+
test "fetch all empty" do
66+
stub_ticks.with("bws --version 2> /dev/null")
67+
stub_login
68+
stub_ticks_with("bws secret list -o env", succeed: false).returns("Error:\n0: Received error message from server")
69+
70+
error = assert_raises RuntimeError do
71+
(shellunescape(run_command("fetch", "all")))
72+
end
73+
assert_equal("Could not read secrets from Bitwarden Secrets Manager", error.message)
74+
end
75+
76+
test "fetch nonexistent item" do
77+
stub_ticks.with("bws --version 2> /dev/null")
78+
stub_login
79+
stub_ticks_with("bws secret get -o env 82aeb5bd-6958-4a89-8197-eacab758acce", succeed: false)
80+
.returns("ERROR (RuntimeError): Could not read 82aeb5bd-6958-4a89-8197-eacab758acce from Bitwarden Secrets Manager")
81+
82+
error = assert_raises RuntimeError do
83+
(shellunescape(run_command("fetch", "82aeb5bd-6958-4a89-8197-eacab758acce")))
84+
end
85+
assert_equal("Could not read 82aeb5bd-6958-4a89-8197-eacab758acce from Bitwarden Secrets Manager", error.message)
86+
end
87+
88+
test "fetch with no access token" do
89+
stub_ticks.with("bws --version 2> /dev/null")
90+
stub_ticks_with("bws run 'echo OK'", succeed: false)
91+
92+
error = assert_raises RuntimeError do
93+
(shellunescape(run_command("fetch", "all")))
94+
end
95+
assert_equal("Could not authenticate to Bitwarden Secrets Manager. Did you set a valid access token?", error.message)
96+
end
97+
98+
test "fetch without CLI installed" do
99+
stub_ticks_with("bws --version 2> /dev/null", succeed: false)
100+
101+
error = assert_raises RuntimeError do
102+
shellunescape(run_command("fetch"))
103+
end
104+
assert_equal "Bitwarden Secrets Manager CLI is not installed", error.message
105+
end
106+
107+
private
108+
def stub_login
109+
stub_ticks.with("bws run 'echo OK'").returns("OK")
110+
end
111+
112+
def run_command(*command)
113+
stdouted do
114+
Kamal::Cli::Secrets.start \
115+
[ *command,
116+
"--adapter", "bitwarden-sm" ]
117+
end
118+
end
119+
end

0 commit comments

Comments
 (0)