Skip to content

Commit a7b2ef5

Browse files
authored
Merge pull request #1189 from egze/enpass
Add support for Enpass - a password manager for secrets
2 parents ea7e72d + 79bc758 commit a7b2ef5

File tree

2 files changed

+149
-0
lines changed

2 files changed

+149
-0
lines changed

lib/kamal/secrets/adapters/enpass.rb

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
##
2+
# Enpass is different from most password managers, in a way that it's offline and doesn't need an account.
3+
#
4+
# Usage
5+
#
6+
# Fetch all password from FooBar item
7+
# `kamal secrets fetch --adapter enpass --from /Users/YOUR_USERNAME/Library/Containers/in.sinew.Enpass-Desktop/Data/Documents/Vaults/primary FooBar`
8+
#
9+
# Fetch only DB_PASSWORD from FooBar item
10+
# `kamal secrets fetch --adapter enpass --from /Users/YOUR_USERNAME/Library/Containers/in.sinew.Enpass-Desktop/Data/Documents/Vaults/primary FooBar/DB_PASSWORD`
11+
class Kamal::Secrets::Adapters::Enpass < Kamal::Secrets::Adapters::Base
12+
def fetch(secrets, account: nil, from:)
13+
check_dependencies!
14+
fetch_secrets(secrets, from)
15+
end
16+
17+
private
18+
def fetch_secrets(secrets, vault)
19+
secrets_titles = fetch_secret_titles(secrets)
20+
21+
result = `enpass-cli -json -vault #{vault.shellescape} show #{secrets_titles.map(&:shellescape).join(" ")}`.strip
22+
23+
parse_result_and_take_secrets(result, secrets)
24+
end
25+
26+
def check_dependencies!
27+
raise RuntimeError, "Enpass CLI is not installed" unless cli_installed?
28+
end
29+
30+
def cli_installed?
31+
`enpass-cli version 2> /dev/null`
32+
$?.success?
33+
end
34+
35+
def fetch_secret_titles(secrets)
36+
secrets.reduce(Set.new) do |secret_titles, secret|
37+
# Sometimes secrets contain a '/', when the intent is to fetch a single password for an item. Example: FooBar/DB_PASSWORD
38+
# Another case is, when the intent is to fetch all passwords for an item. Example: FooBar (and FooBar may have multiple different passwords)
39+
key, separator, value = secret.rpartition("/")
40+
if key.empty?
41+
secret_titles << value
42+
else
43+
secret_titles << key
44+
end
45+
end.to_a
46+
end
47+
48+
def parse_result_and_take_secrets(unparsed_result, secrets)
49+
result = JSON.parse(unparsed_result)
50+
51+
result.reduce({}) do |secrets_with_passwords, item|
52+
title = item["title"]
53+
label = item["label"]
54+
password = item["password"]
55+
56+
if title && password.present?
57+
key = [ title, label ].compact.reject(&:empty?).join("/")
58+
59+
if secrets.include?(title) || secrets.include?(key)
60+
raise RuntimeError, "#{key} is present more than once" if secrets_with_passwords[key]
61+
secrets_with_passwords[key] = password
62+
end
63+
end
64+
65+
secrets_with_passwords
66+
end
67+
end
68+
end

test/secrets/enpass_adapter_test.rb

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
require "test_helper"
2+
3+
class EnpassAdapterTest < SecretAdapterTestCase
4+
test "fetch without CLI installed" do
5+
stub_ticks_with("enpass-cli version 2> /dev/null", succeed: false)
6+
7+
error = assert_raises RuntimeError do
8+
JSON.parse(shellunescape(run_command("fetch", "mynote")))
9+
end
10+
11+
assert_equal "Enpass CLI is not installed", error.message
12+
end
13+
14+
test "fetch one item" do
15+
stub_ticks_with("enpass-cli version 2> /dev/null")
16+
17+
stub_ticks
18+
.with("enpass-cli -json -vault vault-path show FooBar")
19+
.returns(<<~JSON)
20+
[{"category":"computer","label":"SECRET_1","login":"","password":"my-password-1","title":"FooBar","type":"password"}]
21+
JSON
22+
23+
json = JSON.parse(shellunescape(run_command("fetch", "FooBar/SECRET_1")))
24+
25+
expected_json = { "FooBar/SECRET_1" => "my-password-1" }
26+
27+
assert_equal expected_json, json
28+
end
29+
30+
test "fetch multiple items" do
31+
stub_ticks_with("enpass-cli version 2> /dev/null")
32+
33+
stub_ticks
34+
.with("enpass-cli -json -vault vault-path show FooBar")
35+
.returns(<<~JSON)
36+
[
37+
{"category":"computer","label":"SECRET_1","login":"","password":"my-password-1","title":"FooBar","type":"password"},
38+
{"category":"computer","label":"SECRET_2","login":"","password":"my-password-2","title":"FooBar","type":"password"},
39+
{"category":"computer","label":"SECRET_3","login":"","password":"my-password-1","title":"Hello","type":"password"}
40+
]
41+
JSON
42+
43+
json = JSON.parse(shellunescape(run_command("fetch", "FooBar/SECRET_1", "FooBar/SECRET_2")))
44+
45+
expected_json = { "FooBar/SECRET_1" => "my-password-1", "FooBar/SECRET_2" => "my-password-2" }
46+
47+
assert_equal expected_json, json
48+
end
49+
50+
test "fetch all with from" do
51+
stub_ticks_with("enpass-cli version 2> /dev/null")
52+
53+
stub_ticks
54+
.with("enpass-cli -json -vault vault-path show FooBar")
55+
.returns(<<~JSON)
56+
[
57+
{"category":"computer","label":"SECRET_1","login":"","password":"my-password-1","title":"FooBar","type":"password"},
58+
{"category":"computer","label":"SECRET_2","login":"","password":"my-password-2","title":"FooBar","type":"password"},
59+
{"category":"computer","label":"SECRET_3","login":"","password":"my-password-1","title":"Hello","type":"password"},
60+
{"category":"computer","label":"","login":"","password":"my-password-3","title":"FooBar","type":"password"}
61+
]
62+
JSON
63+
64+
json = JSON.parse(shellunescape(run_command("fetch", "FooBar")))
65+
66+
expected_json = { "FooBar/SECRET_1" => "my-password-1", "FooBar/SECRET_2" => "my-password-2", "FooBar" => "my-password-3" }
67+
68+
assert_equal expected_json, json
69+
end
70+
71+
private
72+
def run_command(*command)
73+
stdouted do
74+
Kamal::Cli::Secrets.start \
75+
[ *command,
76+
"-c", "test/fixtures/deploy_with_accessories.yml",
77+
"--adapter", "enpass",
78+
"--from", "vault-path" ]
79+
end
80+
end
81+
end

0 commit comments

Comments
 (0)