require "active_support/configurable" require "active_support/core_ext/object/blank" require "active_support/core_ext/module/delegation" require "active_support/core_ext/hash" require "active_support/core_ext/enumerable" class Muchkeys::ApplicationClient attr_accessor :config, :secret_adapter, :key_validator, :client delegate :certfile_name, :encrypt_string, :decrypt_string, :is_secret?, to: :secret_adapter delegate :valid_key_name?, to: :key_validator delegate :application_name, :consul_url, to: :config def initialize(config = Muchkeys.config) @config = config @secret_adapter = Muchkeys::Secret.new(self) @key_validator = Muchkeys::KeyValidator.new(self) @client = Muchkeys::ConsulClient.new(self) end def allow_unsafe_operation client.unsafe = true yield ensure client.unsafe = false end def set_app_key(key, value, type: nil, **options) set_key(key, value, scope: :application, type: type, **options) end def set_shared_key(key, value, type: nil, **options) set_key(key, value, scope: :shared, type: type, **options) end def set_key(key, value, scope: nil, type: nil, **options) if scope && type key = construct_key_path(key, scope, type) || key end if type == :secret value = secret_adapter.encrypt_string(value.chomp, config.public_key).to_s end client.put(value, key, **options) end def delete_key(key) client.delete(key) end def first(key_name) # http://stackoverflow.com/questions/17853912/ruby-enumerables-is-there-a-detect-for-results-of-block-evaluation # weirdly, this seems to be the most straightforward method of doing this # without a monkey patch, as there is neither a core method that returns # the first non-nil result of evaluating the block, or a lazy compact search_paths(key_name).detect { |path| v = fetch_key(path) and break v } end def all(key_name) search_paths(key_name).map { |path| fetch_key(path) }.compact end def search_paths(key_name = nil) application_search_paths.map { |p| [p, key_name].join('/') } end def fetch_key(key_name, public_key: nil, private_key: nil) if is_secret?(key_name) fetch_secret_key(key_name, public_key, private_key) else fetch_plain_key(key_name) end end def known_keys @known_keys ||= application_search_paths .map { |path| client.get(path, recursive: true) } .compact .each_with_object([]) { |response, keys| keys << parse_recurse_response(response) } .flatten .uniq end def each_path known_keys.each do |key| search_paths(key).each do |path| yield path if fetch_key(path) end end end def verify_keys(*required_keys) if (required_keys - known_keys).any? # if there are any required keys (in the .env file) that are not known # about by the app's consul space, raise an error raise Muchkeys::KeyNotSet, "Consul isn't set with any keys for #{required_keys - known_keys}." end end private def application_search_paths @application_search_paths ||= config.search_paths.collect { |path| path % { application_name: config.application_name } } end def construct_key_path(key, scope, type) found_paths = application_search_paths.select { |path| case when scope == :application && type == :secret path.include?(application_name) && is_secret?(path) when scope == :application && type == :config path.include?(application_name) && !is_secret?(path) when scope == :shared && type == :secret !path.include?(application_name) && is_secret?(path) when scope == :shared && type == :config !path.include?(application_name) && !is_secret?(path) else false end } raise AmbigousPath, "This key could go in multiple folders, please provide full path" if found_paths.many? [found_paths.first, key].join("/") end def parse_recurse_response(response) JSON.parse(response) .collect { |r| r['Key'].rpartition("/").last } .reject(&:empty?) end def fetch_plain_key(key_name) client.get(key_name).presence end def fetch_secret_key(key_name, public_key=nil, private_key=nil) result = fetch_plain_key(key_name) # Don't try to decrypt if the value is nil return nil if result.blank? public_pem = public_key || certfile_name(key_name) private_pem = private_key || certfile_name(key_name) decrypt_string(result, public_pem, private_pem) end end