OnePassword, LastPass + Bitwarden adapters
This commit is contained in:
@@ -2,9 +2,12 @@ class Kamal::Cli::Secrets < Kamal::Cli::Base
|
|||||||
desc "fetch [ITEM] [FIELDS...]", "Fetch secrets from a vault"
|
desc "fetch [ITEM] [FIELDS...]", "Fetch secrets from a vault"
|
||||||
option :adapter, type: :string, aliases: "-a", required: true, desc: "Which vault adapter to use"
|
option :adapter, type: :string, aliases: "-a", required: true, desc: "Which vault adapter to use"
|
||||||
option :account, type: :string, aliases: "-a", required: true, desc: "The account identifier or username"
|
option :account, type: :string, aliases: "-a", required: true, desc: "The account identifier or username"
|
||||||
def fetch(item, *fields)
|
option :location, type: :string, aliases: "-a", required: false, desc: "A vault or folder to fetch the secrets from"
|
||||||
|
def fetch(*secrets)
|
||||||
ENV["KAMAL_SECRETS_KILL_PARENT"] = "1"
|
ENV["KAMAL_SECRETS_KILL_PARENT"] = "1"
|
||||||
puts JSON.dump(adapter(options[:adapter]).fetch(item, fields, account: options[:account])).shellescape
|
|
||||||
|
results = adapter(options[:adapter]).fetch(secrets, **options.slice(:account, :location).symbolize_keys)
|
||||||
|
puts JSON.dump(results).shellescape
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "extract", "Extract a single secret from the results of a fetch call"
|
desc "extract", "Extract a single secret from the results of a fetch call"
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
|
require "active_support/core_ext/string/inflections"
|
||||||
module Kamal::Secrets::Adapters
|
module Kamal::Secrets::Adapters
|
||||||
def self.lookup(name)
|
def self.lookup(name)
|
||||||
case name
|
name = "one_password" if name.downcase == "1password"
|
||||||
when "1password"
|
name = "last_pass" if name.downcase == "lastpass"
|
||||||
Kamal::Secrets::Adapters::OnePassword.new
|
adapter_class(name)
|
||||||
else
|
end
|
||||||
Object.const_get("Kamal::Secrets::Adapters::#{name.camelize}").new
|
|
||||||
end
|
def self.adapter_class(name)
|
||||||
rescue NameError
|
Object.const_get("Kamal::Secrets::Adapters::#{name.camelize}").new
|
||||||
|
rescue NameError => e
|
||||||
raise RuntimeError, "Unknown secrets adapter: #{name}"
|
raise RuntimeError, "Unknown secrets adapter: #{name}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,40 +1,61 @@
|
|||||||
class Kamal::Secrets::Adapters::OnePassword
|
class Kamal::Secrets::Adapters::OnePassword < Kamal::Secrets::Adapters::Base
|
||||||
delegate :optionize, to: Kamal::Utils
|
delegate :optionize, to: Kamal::Utils
|
||||||
|
|
||||||
def fetch(item, fields, account: nil)
|
private
|
||||||
# session may be nil if logging in with the app CLI integration
|
def login(account)
|
||||||
session = signin(account)
|
unless loggedin?(account)
|
||||||
vault, vault_item = item.split("/")
|
`op signin #{to_options(account: account, force: true, raw: true)}`.tap do
|
||||||
labels = fields.map { |field| "label=#{field}" }.join(",")
|
raise RuntimeError, "Failed to login to 1Password" unless $?.success?
|
||||||
options = to_options(vault: vault, fields: labels, format: "json", account: account, session: session.presence)
|
end
|
||||||
|
|
||||||
secrets_json = `op item get #{vault_item} #{options}`.tap do
|
|
||||||
raise RuntimeError, "Could not read #{labels} from #{vault_item} in the #{vault} 1Password vault" unless $?.success?
|
|
||||||
end
|
|
||||||
|
|
||||||
{}.tap do |secrets|
|
|
||||||
JSON.parse(secrets_json).each do |secret_json|
|
|
||||||
# The reference is in the form `op://vault/item/field[/field]`
|
|
||||||
field = secret_json["reference"].delete_prefix("op://#{item}/")
|
|
||||||
secrets[field] = secret_json["value"]
|
|
||||||
secrets[field.split("/").last] = secret_json["value"]
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
rescue => e
|
|
||||||
$stderr.puts " \e[31mERROR (#{e.class}): #{e.message}\e[0m"
|
|
||||||
|
|
||||||
Process.kill("INT", Process.ppid) if ENV["KAMAL_SECRETS_KILL_PARENT"]
|
def loggedin?(account)
|
||||||
exit 1
|
`op account get --account #{account}`
|
||||||
end
|
$?.success?
|
||||||
|
end
|
||||||
|
|
||||||
private
|
def fetch_from_vault(secrets, account:, session:)
|
||||||
def signin(account)
|
{}.tap do |results|
|
||||||
`op signin #{to_options(account: account, force: true, raw: true)}`.tap do
|
vaults_items_fields(secrets).map do |vault, items|
|
||||||
raise RuntimeError, "Failed to login to 1Password" unless $?.success?
|
items.each do |item, fields|
|
||||||
|
fields_json = JSON.parse(op_item_get(vault, item, fields, account: account, session: session))
|
||||||
|
fields_json = [ fields_json ] if fields.one?
|
||||||
|
|
||||||
|
fields_json.each do |field_json|
|
||||||
|
# The reference is in the form `op://vault/item/field[/field]`
|
||||||
|
field = field_json["reference"].delete_suffix("/password")
|
||||||
|
results[field] = field_json["value"]
|
||||||
|
results[field.split("/").last] = field_json["value"]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_options(**options)
|
def to_options(**options)
|
||||||
optionize(options.compact).join(" ")
|
optionize(options.compact).join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def vaults_items_fields(secrets)
|
||||||
|
{}.tap do |vaults|
|
||||||
|
secrets.each do |secret|
|
||||||
|
vault, item, *fields = secret.split("/")
|
||||||
|
fields << "password" if fields.empty?
|
||||||
|
|
||||||
|
vaults[vault] ||= {}
|
||||||
|
vaults[vault][item] ||= []
|
||||||
|
vaults[vault][item] << fields.join(".")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def op_item_get(vault, item, fields, account:, session:)
|
||||||
|
labels = fields.map { |field| "label=#{field}" }.join(",")
|
||||||
|
options = to_options(vault: vault, fields: labels, format: "json", account: account, session: session.presence)
|
||||||
|
|
||||||
|
`op item get #{item} #{options}`.tap do
|
||||||
|
raise RuntimeError, "Could not read #{fields.join(", ")} from #{item} in the #{vault} 1Password vault" unless $?.success?
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user