Merge pull request #1360 from basecamp/secret-adapter-tidy
Secret adapter tidy
This commit is contained in:
@@ -4,9 +4,9 @@ class Kamal::Secrets::Adapters::AwsSecretsManager < Kamal::Secrets::Adapters::Ba
|
|||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_secrets(secrets, account:, session:)
|
def fetch_secrets(secrets, from:, account:, session:)
|
||||||
{}.tap do |results|
|
{}.tap do |results|
|
||||||
get_from_secrets_manager(secrets, account: account).each do |secret|
|
get_from_secrets_manager(prefixed_secrets(secrets, from: from), account: account).each do |secret|
|
||||||
secret_name = secret["Name"]
|
secret_name = secret["Name"]
|
||||||
secret_string = JSON.parse(secret["SecretString"])
|
secret_string = JSON.parse(secret["SecretString"])
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ class Kamal::Secrets::Adapters::Base
|
|||||||
check_dependencies!
|
check_dependencies!
|
||||||
|
|
||||||
session = login(account)
|
session = login(account)
|
||||||
full_secrets = secrets.map { |secret| [ from, secret ].compact.join("/") }
|
fetch_secrets(secrets, from: from, account: account, session: session)
|
||||||
fetch_secrets(full_secrets, account: account, session: session)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def requires_account?
|
def requires_account?
|
||||||
@@ -27,4 +26,8 @@ class Kamal::Secrets::Adapters::Base
|
|||||||
def check_dependencies!
|
def check_dependencies!
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def prefixed_secrets(secrets, from:)
|
||||||
|
secrets.map { |secret| [ from, secret ].compact.join("/") }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ class Kamal::Secrets::Adapters::Bitwarden < Kamal::Secrets::Adapters::Base
|
|||||||
session
|
session
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_secrets(secrets, account:, session:)
|
def fetch_secrets(secrets, from:, account:, session:)
|
||||||
{}.tap do |results|
|
{}.tap do |results|
|
||||||
items_fields(secrets).each do |item, fields|
|
items_fields(prefixed_secrets(secrets, from: from)).each do |item, fields|
|
||||||
item_json = run_command("get item #{item.shellescape}", session: session, raw: true)
|
item_json = run_command("get item #{item.shellescape}", session: session, raw: true)
|
||||||
raise RuntimeError, "Could not read #{item} from Bitwarden" unless $?.success?
|
raise RuntimeError, "Could not read #{item} from Bitwarden" unless $?.success?
|
||||||
item_json = JSON.parse(item_json)
|
item_json = JSON.parse(item_json)
|
||||||
|
|||||||
@@ -9,22 +9,16 @@ class Kamal::Secrets::Adapters::BitwardenSecretsManager < Kamal::Secrets::Adapte
|
|||||||
LIST_COMMAND = "secret list -o env"
|
LIST_COMMAND = "secret list -o env"
|
||||||
GET_COMMAND = "secret get -o env"
|
GET_COMMAND = "secret get -o env"
|
||||||
|
|
||||||
def fetch_secrets(secrets, 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
|
||||||
|
|
||||||
if secrets.length == 1
|
secrets = prefixed_secrets(secrets, from: from)
|
||||||
if secrets[0] == LIST_ALL_SELECTOR
|
command, project = extract_command_and_project(secrets)
|
||||||
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|
|
{}.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}")
|
secret = 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)
|
key, value = parse_secret(secret)
|
||||||
results[key] = value
|
results[key] = value
|
||||||
@@ -40,6 +34,17 @@ class Kamal::Secrets::Adapters::BitwardenSecretsManager < Kamal::Secrets::Adapte
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def extract_command_and_project(secrets)
|
||||||
|
if secrets.length == 1
|
||||||
|
if secrets[0] == LIST_ALL_SELECTOR
|
||||||
|
[ LIST_COMMAND, nil ]
|
||||||
|
elsif secrets[0].end_with?(LIST_ALL_FROM_PROJECT_SUFFIX)
|
||||||
|
project = secrets[0].split(LIST_ALL_FROM_PROJECT_SUFFIX).first
|
||||||
|
[ "#{LIST_COMMAND} #{project.shellescape}", project ]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def parse_secret(secret)
|
def parse_secret(secret)
|
||||||
key, value = secret.split("=", 2)
|
key, value = secret.split("=", 2)
|
||||||
value = value.gsub(/^"|"$/, "")
|
value = value.gsub(/^"|"$/, "")
|
||||||
|
|||||||
@@ -16,8 +16,21 @@ class Kamal::Secrets::Adapters::Doppler < Kamal::Secrets::Adapters::Base
|
|||||||
$?.success?
|
$?.success?
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_secrets(secrets, **)
|
def fetch_secrets(secrets, from:, **)
|
||||||
project_and_config_flags = ""
|
secrets = prefixed_secrets(secrets, from: from)
|
||||||
|
flags = secrets_get_flags(secrets)
|
||||||
|
|
||||||
|
secret_names = secrets.collect { |s| s.split("/").last }
|
||||||
|
|
||||||
|
items = `doppler secrets get #{secret_names.map(&:shellescape).join(" ")} --json #{flags}`
|
||||||
|
raise RuntimeError, "Could not read #{secrets} from Doppler" unless $?.success?
|
||||||
|
|
||||||
|
items = JSON.parse(items)
|
||||||
|
|
||||||
|
items.transform_values { |value| value["computed"] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def secrets_get_flags(secrets)
|
||||||
unless service_token_set?
|
unless service_token_set?
|
||||||
project, config, _ = secrets.first.split("/")
|
project, config, _ = secrets.first.split("/")
|
||||||
|
|
||||||
@@ -27,15 +40,6 @@ class Kamal::Secrets::Adapters::Doppler < Kamal::Secrets::Adapters::Base
|
|||||||
|
|
||||||
project_and_config_flags = "-p #{project.shellescape} -c #{config.shellescape}"
|
project_and_config_flags = "-p #{project.shellescape} -c #{config.shellescape}"
|
||||||
end
|
end
|
||||||
|
|
||||||
secret_names = secrets.collect { |s| s.split("/").last }
|
|
||||||
|
|
||||||
items = `doppler secrets get #{secret_names.map(&:shellescape).join(" ")} --json #{project_and_config_flags}`
|
|
||||||
raise RuntimeError, "Could not read #{secrets} from Doppler" unless $?.success?
|
|
||||||
|
|
||||||
items = JSON.parse(items)
|
|
||||||
|
|
||||||
items.transform_values { |value| value["computed"] }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def service_token_set?
|
def service_token_set?
|
||||||
|
|||||||
@@ -9,16 +9,15 @@
|
|||||||
# Fetch only DB_PASSWORD from FooBar item
|
# Fetch only DB_PASSWORD from FooBar item
|
||||||
# `kamal secrets fetch --adapter enpass --from /Users/YOUR_USERNAME/Library/Containers/in.sinew.Enpass-Desktop/Data/Documents/Vaults/primary FooBar/DB_PASSWORD`
|
# `kamal secrets fetch --adapter enpass --from /Users/YOUR_USERNAME/Library/Containers/in.sinew.Enpass-Desktop/Data/Documents/Vaults/primary FooBar/DB_PASSWORD`
|
||||||
class Kamal::Secrets::Adapters::Enpass < Kamal::Secrets::Adapters::Base
|
class Kamal::Secrets::Adapters::Enpass < Kamal::Secrets::Adapters::Base
|
||||||
def fetch(secrets, account: nil, from:)
|
def requires_account?
|
||||||
check_dependencies!
|
false
|
||||||
fetch_secrets(secrets, from)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def fetch_secrets(secrets, vault)
|
def fetch_secrets(secrets, from:, account:, session:)
|
||||||
secrets_titles = fetch_secret_titles(secrets)
|
secrets_titles = fetch_secret_titles(secrets)
|
||||||
|
|
||||||
result = `enpass-cli -json -vault #{vault.shellescape} show #{secrets_titles.map(&:shellescape).join(" ")}`.strip
|
result = `enpass-cli -json -vault #{from.shellescape} show #{secrets_titles.map(&:shellescape).join(" ")}`.strip
|
||||||
|
|
||||||
parse_result_and_take_secrets(result, secrets)
|
parse_result_and_take_secrets(result, secrets)
|
||||||
end
|
end
|
||||||
@@ -32,6 +31,10 @@ class Kamal::Secrets::Adapters::Enpass < Kamal::Secrets::Adapters::Base
|
|||||||
$?.success?
|
$?.success?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def login(account)
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
def fetch_secret_titles(secrets)
|
def fetch_secret_titles(secrets)
|
||||||
secrets.reduce(Set.new) do |secret_titles, secret|
|
secrets.reduce(Set.new) do |secret_titles, secret|
|
||||||
# Sometimes secrets contain a '/', when the intent is to fetch a single password for an item. Example: FooBar/DB_PASSWORD
|
# Sometimes secrets contain a '/', when the intent is to fetch a single password for an item. Example: FooBar/DB_PASSWORD
|
||||||
|
|||||||
@@ -18,19 +18,19 @@ class Kamal::Secrets::Adapters::GcpSecretManager < Kamal::Secrets::Adapters::Bas
|
|||||||
# - "default|my-service-user@example.com" will use the default user, and enable service account impersonation as my-service-user
|
# - "default|my-service-user@example.com" will use the default user, and enable service account impersonation as my-service-user
|
||||||
# - "default|my-service-user@example.com,another-service-user@example.com" same as above, but with an impersonation delegation chain
|
# - "default|my-service-user@example.com,another-service-user@example.com" same as above, but with an impersonation delegation chain
|
||||||
|
|
||||||
if !logged_in?
|
unless logged_in?
|
||||||
`gcloud auth login`
|
`gcloud auth login`
|
||||||
raise RuntimeError, "gcloud is not authenticated, please run `gcloud auth login`" if !logged_in?
|
raise RuntimeError, "could not login to gcloud" unless logged_in?
|
||||||
end
|
end
|
||||||
|
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_secrets(secrets, account:, session:)
|
def fetch_secrets(secrets, from:, account:, session:)
|
||||||
user, service_account = parse_account(account)
|
user, service_account = parse_account(account)
|
||||||
|
|
||||||
{}.tap do |results|
|
{}.tap do |results|
|
||||||
secrets_with_metadata(secrets).each do |secret, (project, secret_name, secret_version)|
|
secrets_with_metadata(prefixed_secrets(secrets, from: from)).each do |secret, (project, secret_name, secret_version)|
|
||||||
item_name = "#{project}/#{secret_name}"
|
item_name = "#{project}/#{secret_name}"
|
||||||
results[item_name] = fetch_secret(project, secret_name, secret_version, user, service_account)
|
results[item_name] = fetch_secret(project, secret_name, secret_version, user, service_account)
|
||||||
raise RuntimeError, "Could not read #{item_name} from Google Secret Manager" unless $?.success?
|
raise RuntimeError, "Could not read #{item_name} from Google Secret Manager" unless $?.success?
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ class Kamal::Secrets::Adapters::LastPass < Kamal::Secrets::Adapters::Base
|
|||||||
`lpass status --color never`.strip == "Logged in as #{account}."
|
`lpass status --color never`.strip == "Logged in as #{account}."
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_secrets(secrets, account:, session:)
|
def fetch_secrets(secrets, from:, account:, session:)
|
||||||
|
secrets = prefixed_secrets(secrets, from: from)
|
||||||
items = `lpass show #{secrets.map(&:shellescape).join(" ")} --json`
|
items = `lpass show #{secrets.map(&:shellescape).join(" ")} --json`
|
||||||
raise RuntimeError, "Could not read #{secrets} from LastPass" unless $?.success?
|
raise RuntimeError, "Could not read #{secrets} from LastPass" unless $?.success?
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ class Kamal::Secrets::Adapters::OnePassword < Kamal::Secrets::Adapters::Base
|
|||||||
$?.success?
|
$?.success?
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_secrets(secrets, account:, session:)
|
def fetch_secrets(secrets, from:, account:, session:)
|
||||||
{}.tap do |results|
|
{}.tap do |results|
|
||||||
vaults_items_fields(secrets).map do |vault, items|
|
vaults_items_fields(prefixed_secrets(secrets, from: from)).map do |vault, items|
|
||||||
items.each do |item, fields|
|
items.each do |item, fields|
|
||||||
fields_json = JSON.parse(op_item_get(vault, item, fields, account: account, session: session))
|
fields_json = JSON.parse(op_item_get(vault, item, fields, account: account, session: session))
|
||||||
fields_json = [ fields_json ] if fields.one?
|
fields_json = [ fields_json ] if fields.one?
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ class Kamal::Secrets::Adapters::Test < Kamal::Secrets::Adapters::Base
|
|||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_secrets(secrets, account:, session:)
|
def fetch_secrets(secrets, from:, account:, session:)
|
||||||
secrets.to_h { |secret| [ secret, secret.reverse ] }
|
prefixed_secrets(secrets, from: from).to_h { |secret| [ secret, secret.reverse ] }
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_dependencies!
|
def check_dependencies!
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
class Kamal::Secrets::Adapters::TestOptionalAccount < Kamal::Secrets::Adapters::Test
|
|
||||||
def requires_account?
|
|
||||||
false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -13,12 +13,6 @@ class CliSecretsTest < CliTestCase
|
|||||||
run_command("fetch", "foo", "bar", "baz", "--adapter", "test")
|
run_command("fetch", "foo", "bar", "baz", "--adapter", "test")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "fetch without required --account" do
|
|
||||||
assert_equal \
|
|
||||||
"\\{\\\"foo\\\":\\\"oof\\\",\\\"bar\\\":\\\"rab\\\",\\\"baz\\\":\\\"zab\\\"\\}",
|
|
||||||
run_command("fetch", "foo", "bar", "baz", "--adapter", "test_optional_account")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "extract" do
|
test "extract" do
|
||||||
assert_equal "oof", run_command("extract", "foo", "{\"foo\":\"oof\", \"bar\":\"rab\", \"baz\":\"zab\"}")
|
assert_equal "oof", run_command("extract", "foo", "{\"foo\":\"oof\", \"bar\":\"rab\", \"baz\":\"zab\"}")
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class GcpSecretManagerAdapterTest < SecretAdapterTestCase
|
|||||||
JSON.parse(shellunescape(run_command("fetch", "mypassword")))
|
JSON.parse(shellunescape(run_command("fetch", "mypassword")))
|
||||||
end
|
end
|
||||||
|
|
||||||
assert_match(/not authenticated/, error.message)
|
assert_match(/could not login to gcloud/, error.message)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "fetch with from" do
|
test "fetch with from" do
|
||||||
|
|||||||
Reference in New Issue
Block a user