require "muchkeys/version" require "muchkeys/errors" require "muchkeys/configuration" require "muchkeys/secret" require "muchkeys/key_validator" require "muchkeys/cli" require "muchkeys/cli/validator" require "net/http" module MuchKeys class << self attr_accessor :configuration def configure self.configuration ||= MuchKeys::Configuration.new if block_given? yield configuration end 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 end def find_first(key_name) return ENV[key_name] unless ENV[key_name].nil? unless MuchKeys.configuration.search_paths application_name = detect_app_name return false if !application_name end consul_paths = search_order(application_name, key_name) response = nil search_order(application_name, key_name).detect do |consul_path| response = fetch_key(consul_path) end if !response 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 # empty and unset keys from consul are empty strings, so return false # here TODO: UnsetKey would be better here. return false if response == "" # otherwise, we got consul data so return that response end def consul_url(key_name) URI("#{configuration.consul_url}/v1/kv/#{key_name}?raw") end # TODO: inline this def fetch_body(response) response.body end def handle_response(response_code) case response_code when (400..599) raise MuchKeys::InvalidKey end end private def fetch_plain_key(key_name) url = consul_url(key_name) response = Net::HTTP.get_response url handle_response(response) fetch_body(response) end def fetch_secret_key(key_name, public_pem=nil, private_pem=nil) result = fetch_plain_key(key_name) # FIXME: omg use a class like MuchKeys::UnsetKey instead of "" as a # return value -- there are other places like this too. # we hit a key that doesn't exist, so don't try to decrypt it return "" if !result || result.empty? 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. # TODO: figure out how to detect plain Rack apps. :( def detect_app_name return configuration.application_name if configuration.application_name # Rails.application is "Monorail::Application". if defined?(Rails) application_name = Rails.application.class.to_s.split("::").first elsif defined?(Rack) application_name = "Asset_Server" else $stderr.puts "can't detect app name" return 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.[](i) MuchKeys.find_first(i) end end