require "muchkeys/version" require "muchkeys/errors" require "muchkeys/configuration" require "muchkeys/secret" require "muchkeys/key_validator" require "muchkeys/cli" require "net/http" module MuchKeys # revealing intention module BlankKey; false; end class << self attr_accessor :configuration def configure self.configuration ||= MuchKeys::Configuration.new if block_given? yield configuration end end # this is the entry point to the ENV-like object that devs will use def find_first(key_name) unless MuchKeys.configuration.search_paths raise MuchKeys::UnknownApplication, "Can't detect app name and application_name isn't set." if !application_name end consul_paths_to_search = search_order(application_name, key_name) # search consul in a specific order until we find something response = nil consul_paths_to_search.detect do |consul_path| response = fetch_key(consul_path) response if response != MuchKeys::BlankKey end if response == BlankKey raise MuchKeys::NoKeysSet, "Bailing. Consul isn't set with any keys for #{key_name}." end response end def fetch_key(key_name, public_key:nil, private_key:nil) return ENV[key_name] unless ENV[key_name].nil? if secret_adapter.is_secret?(key_name) raise InvalidKey unless validate_key_name(key_name) # configure automatic certificates public_key = find_certfile_for_key(key_name) if public_key.nil? private_key = find_certfile_for_key(key_name) if private_key.nil? response = fetch_secret_key(key_name, public_key, private_key) else response = fetch_plain_key(key_name) end # otherwise, we got consul data so return that response end def search_order(application_name, key_name) if MuchKeys.configuration.search_paths search_paths = MuchKeys.configuration.search_paths.collect do |path| "#{path}/#{key_name}" end else search_paths = [ "git/#{application_name}/secrets/#{key_name}", "git/#{application_name}/config/#{key_name}", "git/shared/secrets/#{key_name}", "git/shared/config/#{key_name}" ] end search_paths end private def consul_url(key_name) URI("#{configuration.consul_url}/v1/kv/#{key_name}?raw") end def fetch_plain_key(key_name) url = consul_url(key_name) begin response = Net::HTTP.get_response url return MuchKeys::BlankKey if !response.body || response.body.empty? response.body rescue Errno::ECONNREFUSED return nil end end def fetch_secret_key(key_name, public_pem=nil, private_pem=nil) result = fetch_plain_key(key_name) # we hit a key that doesn't exist, so don't try to decrypt it return MuchKeys::BlankKey if result == MuchKeys::BlankKey || result.nil? secret_adapter.decrypt_string(result, public_pem, private_pem) end def find_certfile_for_key(key_name) secret_adapter.certfile_name key_name end def secret_adapter MuchKeys::Secret end def validate_key_name(key_name) MuchKeys::KeyValidator.valid? key_name end # Detecting Rails app names is a known quantity. # Rack apps need to set the name through config. def application_name return configuration.application_name if configuration.application_name if defined?(Rails) # Rails.application looks something like "Monorail::Application" application_name = Rails.application.class.to_s.split("::").first else return false end snakecase(application_name) end # MyApp should become my_app def snakecase(string) string.gsub(/::/, '/'). gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). gsub(/([a-z\d])([A-Z])/,'\1_\2'). tr("-", "_"). downcase end end end # default configure the gem on gem load MuchKeys.configuration ||= MuchKeys::Configuration.new # This interface looks like ENV which is more friendly to devs class MUCHKEYS def self.[](key_name) # if an ENV key is set, use that first return ENV[key_name] unless ENV[key_name].nil? MuchKeys.find_first(key_name) end end