muchkeys 0.5.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 78189f815bc07abd0764251e38f50d803325dd32
4
- data.tar.gz: d1c41a5c3a82c5f2e97b4a434ee25f38ffaa7cff
3
+ metadata.gz: e81c8deded76555843a5242334ae71417521d1f3
4
+ data.tar.gz: 3c71a228e84eb175b81cec46fa903140dcf76006
5
5
  SHA512:
6
- metadata.gz: bb82cbe29730794b1bcc05005b9761b24723fc58fd551b70a7f3743ddd266ce9cebdd97b54ad9fdd783651c8785c3cb831835554c078039e19b8a1a7e9244bc3
7
- data.tar.gz: 2ba20955c3daec345019303d176081881fa9da8a69837f4a2e1b7196635414d5d5eae1fd0be45651fb86c399aed3482a3c582b411b7bcdec2418c09503377777
6
+ metadata.gz: c635fa24d92c441d5e777238c3beed8472942704a8efb0d907e504ec2f72f08ad6ab6c5ee02ffe61bd81405b277c414a8341d1041e998cde631b85b7b16a896d
7
+ data.tar.gz: 572192537944c62a1960be30603f9be0cc0e6de4a5383af8f4c271a47d9395a1a5cf30137d65260c6f667bff813e3617c061415f860b59ed25ca7e24f122815c
data/Dockerfile ADDED
@@ -0,0 +1,16 @@
1
+ # pull our ruby version
2
+ FROM ruby:2.3.1-alpine
3
+ MAINTAINER goldstarevents
4
+
5
+ RUN apk update && apk add bash build-base git gcc abuild binutils binutils-doc gcc-doc
6
+ RUN gem install bundler
7
+
8
+ WORKDIR /gem/
9
+ ADD . /gem/
10
+ RUN bundle install
11
+
12
+ VOLUME .:/gem/
13
+
14
+ ENTRYPOINT ["bundle", "exec"]
15
+ CMD ["rake", "-T"]
16
+
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # MuchKeys
1
+ # Muchkeys
2
2
 
3
3
  ─────────▄──────────────▄
4
4
  ────────▌▒█───────────▄▀▒▌
@@ -21,7 +21,7 @@
21
21
  ──▐▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▀▀
22
22
 
23
23
 
24
- MuchKeys lets you store your application keys in consul and then leverages many conventions to create a
24
+ Muchkeys lets you store your application keys in consul and then leverages many conventions to create a
25
25
  pleasant API. Keys in this context can mean application settings, knobs, dials, passwords, api keys
26
26
  or anything. It's primary use is in production where it will check to see if a ENV variable exists first and then
27
27
  search consul for a key based on an opinionated hierarchy convention.
@@ -29,7 +29,7 @@ search consul for a key based on an opinionated hierarchy convention.
29
29
  You will want to read below about the convention and assumptions first to see if this works for
30
30
  you.
31
31
 
32
- MuchKeys also has a way of using encrypted secrets stored in consul. MuchKeys has a command line interface to help you encrypt or read secrets stored in consul.
32
+ Muchkeys also has a way of using encrypted secrets stored in consul. Muchkeys has a command line interface to help you encrypt or read secrets stored in consul.
33
33
 
34
34
 
35
35
  ## Installation
@@ -62,10 +62,10 @@ a central configuration store while retaining control of your local development
62
62
  <%= MUCHKEYS['widget_api_key'] %>
63
63
  ```
64
64
 
65
- Now the `widget_api_key` is coming from a central location. Devs can override `widget_api_key` with an ENV setting. `export WIDGET_API_KEY="development_key76"` MuchKeys defers to ENV when
65
+ Now the `widget_api_key` is coming from a central location. Devs can override `widget_api_key` with an ENV setting. `export WIDGET_API_KEY="development_key76"` Muchkeys defers to ENV when
66
66
  it sees one set.
67
67
 
68
- MuchKeys will look in consul (a key/value store) for keys (settings/secrets/knobs to turn).
68
+ Muchkeys will look in consul (a key/value store) for keys (settings/secrets/knobs to turn).
69
69
  It searches in an order of convention. It is also assuming you want to use git2consul
70
70
  to enable your developers to add their own keys. Git2consul syncs a git repo to consul.
71
71
 
@@ -82,7 +82,7 @@ and this is something new to the app.
82
82
  * Ops wasn't involved. Developers are empowered.
83
83
 
84
84
 
85
- MuchKeys does this magic through a search order. It searches consul for the key
85
+ Muchkeys does this magic through a search order. It searches consul for the key
86
86
  in certain order: (notice that the key isn't prefixed when you look up `twitter_api_key` with
87
87
  `MUCHKEYS['twitter_api_key']`)
88
88
 
@@ -99,7 +99,7 @@ The above `feed_reader` is detected from `Rails.application`. If you don't have
99
99
  the application name in the configure block:
100
100
 
101
101
  ```
102
- MuchKeys.configure do |config|
102
+ Muchkeys.configure do |config|
103
103
  config.application_name = "rack_app"
104
104
  end
105
105
  ```
@@ -107,7 +107,7 @@ end
107
107
  The order and paths that it searches is configurable:
108
108
 
109
109
  ```
110
- MuchKeys.configure do |config|
110
+ Muchkeys.configure do |config|
111
111
  config.search_paths = %W(
112
112
  app_name/keys app_name/secrets shared/keys shared/secrets
113
113
  )
@@ -129,7 +129,7 @@ your keys and secrets organized like this (one deep nesting). This is configura
129
129
  ```
130
130
  # if you don't put your secrets in something like shared/secrets/
131
131
  # but shared/passwords/
132
- MuchKeys.configure do |config|
132
+ Muchkeys.configure do |config|
133
133
  config.secrets_path_hint = "passwords/"
134
134
  end
135
135
  ```
@@ -148,7 +148,7 @@ done. If you need to override defaults, create an initializer.
148
148
 
149
149
  ```
150
150
  # config/initializers/muchkeys.rb
151
- MuchKeys.configure do |config|
151
+ Muchkeys.configure do |config|
152
152
  # your settings if desired
153
153
  end
154
154
  ```
@@ -179,12 +179,12 @@ openssl req -new -newkey rsa:4096 -nodes -x509 -keyout /tmp/self_signed_test.pem
179
179
  ```
180
180
 
181
181
  ```ruby
182
- MuchKeys.configure do |config|
182
+ Muchkeys.configure do |config|
183
183
  config.public_key = "/tmp/self_signed_test.pem"
184
184
  config.private_key = "/tmp/self_signed_test.pem"
185
185
  end
186
186
 
187
- puts MuchKeys::Secret.encrypt_string("bacon is just ok").to_s
187
+ puts Muchkeys::Secret.encrypt_string("bacon is just ok").to_s
188
188
  # => -----BEGIN PKCS7-----
189
189
  # => MIICuwYJKoZIhvcNAQcDoIICrDCCAqgCAQAxggJuMIICagIBADBSMEUxCzAJ ...
190
190
  # => ...
@@ -192,28 +192,28 @@ puts MuchKeys::Secret.encrypt_string("bacon is just ok").to_s
192
192
  # Copy and paste this into consul under a key: ie: secrets/fake
193
193
 
194
194
  # Now you can fetch the secret with the same public key.
195
- MuchKeys::Secret.get_consul_secret('secrets/fake')
195
+ Muchkeys::Secret.get_consul_secret('secrets/fake')
196
196
  # => bacon is just ok
197
197
  ```
198
198
 
199
199
  Inside your app:
200
200
  ```ruby
201
201
  # inside a rails initializer or other setup file you have
202
- MuchKeys.configure do |config|
202
+ Muchkeys.configure do |config|
203
203
  config.consul_url = "http://myrealhost" # Default is "http://localhost:8500"
204
204
  config.public_key = "path to public .pem" # this is only required if you are encrypting secrets
205
205
  config.private_key = "path to private .pem" # this is only required if you are encrypting secrets
206
206
  end
207
207
 
208
208
  # then inside your YAML or app:
209
- MuchKeys.fetch_key('number_of_threads')
209
+ Muchkeys.fetch_key('number_of_threads')
210
210
  # goes to git/config/myapp/number_of_threads in consul
211
211
  ```
212
212
 
213
213
 
214
214
  ## Automagic Certificates
215
215
 
216
- MuchKeys can find certs automatically in `~/.keys`. `MuchKeys.fetch_key("git/waffles/secrets/a_password")` will try to decrypt `a_password`
216
+ Muchkeys can find certs automatically in `~/.keys`. `Muchkeys.fetch_key("git/waffles/secrets/a_password")` will try to decrypt `a_password`
217
217
  with a cert called `~/.keys/waffles.pem`. The private key can be in the same file but should be protected
218
218
  with file permissions `0600`.
219
219
 
@@ -232,12 +232,12 @@ $ muchkeys -d --public_key=~/.keys/staging.pem --private_key=~/.keys/staging.pem
232
232
  You can fetch individual keys if you want.
233
233
 
234
234
  ```bash
235
- ruby -e 'require "muchkeys"; puts MuchKeys.fetch_key("mail_server")'
235
+ ruby -e 'require "muchkeys"; puts Muchkeys.fetch_key("mail_server")'
236
236
  # => smtp.example.com (from consul)
237
237
  ```
238
238
 
239
239
  ```bash
240
- mail_server=muffin.ninja.local ruby -e 'require "muchkeys"; puts MuchKeys.fetch_key("mail_server")'
240
+ mail_server=muffin.ninja.local ruby -e 'require "muchkeys"; puts Muchkeys.fetch_key("mail_server")'
241
241
  # => muffin.ninja.local (from ENV)
242
242
  ```
243
243
 
@@ -257,7 +257,7 @@ to change a setting and some things might be shared between apps like URLs, api
257
257
 
258
258
  Searching multiple paths in consul is because consul is hierarchical (or can be) and ENV is not.
259
259
  Making an easy to use API that looks and behaves like `ENV` was one of the goals to make
260
- it more familiar to developers. So worst case, by default, MuchKeys will search consul 4 times
260
+ it more familiar to developers. So worst case, by default, Muchkeys will search consul 4 times
261
261
  to find a key.
262
262
 
263
263
  Encrypted secrets can be slightly more clunky. Developers may not have signing keys,
data/circle.yml CHANGED
@@ -1,10 +1,26 @@
1
1
  machine:
2
- ruby:
3
- version: 2.1.6
2
+ pre:
3
+ - curl -sSL https://s3.amazonaws.com/circle-downloads/install-circleci-docker.sh | bash -s -- 1.10.0
4
+ services:
5
+ - docker
4
6
  timezone:
5
7
  Etc/GMT
6
8
 
7
- checkout:
8
- post:
9
- - gem install bundler -v 1.12.4
9
+ dependencies:
10
+ override:
11
+ - docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS quay.io
12
+ - sudo apt-get install python-dev
13
+ - sudo pip install --upgrade docker-compose==1.8.0
10
14
 
15
+ database:
16
+ override:
17
+ - echo "no db for you"
18
+
19
+ test:
20
+ pre:
21
+ - docker-compose build
22
+ override:
23
+ - docker-compose run default bash -c -l "bundle exec rspec":
24
+ parallel: true
25
+ files:
26
+ - spec/**/*_spec.rb
@@ -0,0 +1,15 @@
1
+ version: '2'
2
+ services:
3
+
4
+ consul:
5
+ image: consul
6
+ ports:
7
+ - '8500:8500'
8
+
9
+ default:
10
+ build: .
11
+ command: bash -l -c 'sleep 2 && bundle exec rspec spec'
12
+ links:
13
+ - consul
14
+ volumes:
15
+ - .:/gem/
data/exe/muchkeys CHANGED
@@ -2,4 +2,4 @@
2
2
 
3
3
  require "muchkeys"
4
4
 
5
- MuchKeys::CLI.start
5
+ Muchkeys::CLI.start
@@ -0,0 +1,151 @@
1
+ require "active_support/configurable"
2
+ require "active_support/core_ext/object/blank"
3
+ require "active_support/core_ext/module/delegation"
4
+ require "active_support/core_ext/hash"
5
+ require "active_support/core_ext/enumerable"
6
+
7
+ class Muchkeys::ApplicationClient
8
+ attr_accessor :config, :secret_adapter, :key_validator, :client
9
+
10
+ delegate :certfile_name, :encrypt_string, :decrypt_string, :is_secret?, to: :secret_adapter
11
+ delegate :valid_key_name?, to: :key_validator
12
+ delegate :application_name, :consul_url, to: :config
13
+
14
+ def initialize(config = Muchkeys.config)
15
+ @config = config
16
+ @secret_adapter = Muchkeys::Secret.new(self)
17
+ @key_validator = Muchkeys::KeyValidator.new(self)
18
+ @client = Muchkeys::ConsulClient.new(self)
19
+ end
20
+
21
+ def allow_unsafe_operation
22
+ client.unsafe = true
23
+ yield
24
+ ensure
25
+ client.unsafe = false
26
+ end
27
+
28
+ def set_app_key(key, value, type: nil, **options)
29
+ set_key(key, value, scope: :application, type: type, **options)
30
+ end
31
+
32
+ def set_shared_key(key, value, type: nil, **options)
33
+ set_key(key, value, scope: :shared, type: type, **options)
34
+ end
35
+
36
+ def set_key(key, value, scope: nil, type: nil, **options)
37
+ if scope && type
38
+ key = construct_key_path(key, scope, type) || key
39
+ end
40
+
41
+ if type == :secret
42
+ value = secret_adapter.encrypt_string(value.chomp, config.public_key).to_s
43
+ end
44
+
45
+ client.put(value, key, **options)
46
+ end
47
+
48
+ def delete_key(key)
49
+ client.delete(key)
50
+ end
51
+
52
+ def first(key_name)
53
+ # http://stackoverflow.com/questions/17853912/ruby-enumerables-is-there-a-detect-for-results-of-block-evaluation
54
+ # weirdly, this seems to be the most straightforward method of doing this
55
+ # without a monkey patch, as there is neither a core method that returns
56
+ # the first non-nil result of evaluating the block, or a lazy compact
57
+ search_paths(key_name).detect { |path| v = fetch_key(path) and break v }
58
+ end
59
+
60
+ def all(key_name)
61
+ search_paths(key_name).map { |path| fetch_key(path) }.compact
62
+ end
63
+
64
+ def search_paths(key_name = nil)
65
+ application_search_paths.map { |p| [p, key_name].join('/') }
66
+ end
67
+
68
+ def fetch_key(key_name, public_key: nil, private_key: nil)
69
+ if is_secret?(key_name)
70
+ fetch_secret_key(key_name, public_key, private_key)
71
+ else
72
+ fetch_plain_key(key_name)
73
+ end
74
+ end
75
+
76
+ def known_keys
77
+ @known_keys ||= application_search_paths
78
+ .map { |path| client.get(path, recursive: true) }
79
+ .compact
80
+ .each_with_object([]) { |response, keys| keys << parse_recurse_response(response) }
81
+ .flatten
82
+ .uniq
83
+ end
84
+
85
+ def each_path
86
+ known_keys.each do |key|
87
+ search_paths(key).each do |path|
88
+ yield path if fetch_key(path)
89
+ end
90
+ end
91
+ end
92
+
93
+ def verify_keys(*required_keys)
94
+ if (required_keys - known_keys).any?
95
+ # if there are any required keys (in the .env file) that are not known
96
+ # about by the app's consul space, raise an error
97
+ raise Muchkeys::KeyNotSet, "Consul isn't set with any keys for #{required_keys - known_keys}."
98
+ end
99
+ end
100
+
101
+ private
102
+
103
+ def application_search_paths
104
+ @application_search_paths ||= config.search_paths.collect { |path|
105
+ path % { application_name: config.application_name }
106
+ }
107
+ end
108
+
109
+ def construct_key_path(key, scope, type)
110
+ found_paths = application_search_paths.select { |path|
111
+ case
112
+ when scope == :application && type == :secret
113
+ path.include?(application_name) && is_secret?(path)
114
+ when scope == :application && type == :config
115
+ path.include?(application_name) && !is_secret?(path)
116
+ when scope == :shared && type == :secret
117
+ !path.include?(application_name) && is_secret?(path)
118
+ when scope == :shared && type == :config
119
+ !path.include?(application_name) && !is_secret?(path)
120
+ else
121
+ false
122
+ end
123
+ }
124
+
125
+ raise AmbigousPath, "This key could go in multiple folders, please provide full path" if found_paths.many?
126
+
127
+ [found_paths.first, key].join("/")
128
+ end
129
+
130
+ def parse_recurse_response(response)
131
+ JSON.parse(response)
132
+ .collect { |r| r['Key'].rpartition("/").last }
133
+ .reject(&:empty?)
134
+ end
135
+
136
+ def fetch_plain_key(key_name)
137
+ client.get(key_name).presence
138
+ end
139
+
140
+ def fetch_secret_key(key_name, public_key=nil, private_key=nil)
141
+ result = fetch_plain_key(key_name)
142
+
143
+ # Don't try to decrypt if the value is nil
144
+ return nil if result.blank?
145
+
146
+ public_pem = public_key || certfile_name(key_name)
147
+ private_pem = private_key || certfile_name(key_name)
148
+
149
+ decrypt_string(result, public_pem, private_pem)
150
+ end
151
+ end
data/lib/muchkeys/cli.rb CHANGED
@@ -1,50 +1,145 @@
1
1
  require 'thor'
2
2
 
3
- module MuchKeys
3
+ module Muchkeys
4
4
  class CLI < Thor
5
5
  include Thor::Actions
6
6
 
7
7
  map %w[--version -v] => :__version
8
8
 
9
- class_option :consule_url, type: :string, default: 'http://localhost:8500'
9
+ class_option :consul_url, type: :string, default: 'http://localhost:8500'
10
10
 
11
11
  desc "encrypt FILE", "encrypt keys from a file to put in consul"
12
12
  method_option :public_key, type: :string, required: true
13
13
  def encrypt(file)
14
- say MuchKeysExecutor.encrypt(file, options[:public_key])
14
+ Muchkeys.configure { |c| c.consul_url = options[:consul_url] }
15
+ say MuchkeysExecutor.encrypt(file, options[:public_key])
15
16
  end
16
17
 
17
18
  desc "decrypt KEY", "decrypt keys from consul"
18
19
  method_option :public_key, type: :string, required: true
19
20
  method_option :private_key, type: :string, required: true
20
21
  def decrypt(consul_key)
21
- say MuchKeysExecutor.decrypt(consul_key, options[:public_key], options[:private_key])
22
+ Muchkeys.configure { |c| c.consul_url = options[:consul_url] }
23
+ say MuchkeysExecutor.decrypt(consul_key, options[:public_key], options[:private_key])
24
+ end
25
+
26
+ desc "check", "ensure that all keys in .env in CWD are present in consul"
27
+ def check(app_name)
28
+ Muchkeys.configure { |c| c.consul_url = options[:consul_url] }
29
+ say MuchkeysExecutor.check(app_name)
30
+ end
31
+
32
+ desc "list", "list all keys in Application"
33
+ def list(app_name)
34
+ Muchkeys.configure { |c| c.consul_url = options[:consul_url] }
35
+ say MuchkeysExecutor.list(app_name)
22
36
  end
23
37
 
24
38
  desc "fetch KEY", "fetch plaintext key from consul"
25
39
  def fetch(consul_key)
26
- say MuchKeysExecutor.fetch(consul_key)
40
+ Muchkeys.configure { |c| c.consul_url = options[:consul_url] }
41
+ say MuchkeysExecutor.fetch(consul_key)
42
+ end
43
+
44
+ desc "store DATA KEY", "store data a particular key"
45
+ method_option :public_key, type: :string
46
+ method_option :private_key, type: :string
47
+ def store(data, consul_key)
48
+ Muchkeys.configure { |c| c.consul_url = options[:consul_url] }
49
+ say MuchkeysExecutor.store(consul_key, data, public_key: options[:public_key], private_key: options[:private_key])
50
+ end
51
+
52
+ desc "wipeout", "clear Consul"
53
+ method_option :app_name, type: :string
54
+ def wipeout
55
+ Muchkeys.configure { |c| c.consul_url = options[:consul_url] }
56
+ unless yes?("Really clear consul instance at #{Muchkeys.config.consul_url}?", Thor::Shell::Color::RED)
57
+ say "Nothing done!"
58
+ else
59
+ say MuchkeysExecutor.wipeout(app_name: options[:app_name])
60
+ end
61
+ end
62
+
63
+ desc "delete", "remove key"
64
+ def delete(key)
65
+ Muchkeys.configure { |c| c.consul_url = options[:consul_url] }
66
+ say MuchkeysExecutor.delete(key)
27
67
  end
28
68
 
29
69
  desc "--version", "Print the version"
30
70
  def __version
31
- say MuchKeys::VERSION
71
+ Muchkeys.configure { |c| c.consul_url = options[:consul_url] }
72
+ say Muchkeys::VERSION
32
73
  end
33
74
 
34
- module MuchKeysExecutor
75
+ module MuchkeysExecutor
35
76
  extend self
36
77
 
37
78
  def encrypt(file, public_key)
38
79
  string_to_encrypt = File.read(file)
39
- MuchKeys::Secret.encrypt_string(string_to_encrypt, public_key)
80
+ Muchkeys::ApplicationClient.new.secret_adapter.encrypt_string(string_to_encrypt.chomp, public_key)
40
81
  end
41
82
 
42
83
  def decrypt(consul_key, public_key, private_key)
43
- MuchKeys.fetch_key(consul_key, public_key: public_key, private_key:private_key)
84
+ Muchkeys::ApplicationClient.new.fetch_key(consul_key, public_key: public_key, private_key: private_key)
85
+ end
86
+
87
+ def store(consul_key, data, public_key: nil, private_key: nil)
88
+ Muchkeys.configure { |c| c.public_key = public_key; c.private_key = private_key }
89
+ app = Muchkeys::ApplicationClient.new(Muchkeys.config)
90
+
91
+ if data == "-"
92
+ data = $stdin.read
93
+ end
94
+
95
+ app.allow_unsafe_operation do
96
+ if public_key && private_key
97
+ app.set_key(consul_key, data, type: :secret)
98
+ "Secret '#{data}' stored in #{consul_key}"
99
+ else
100
+ app.set_key(consul_key, data)
101
+ "'#{data}' stored in #{consul_key}"
102
+ end
103
+ end
104
+ end
105
+
106
+ def list(app_name)
107
+ Muchkeys.configure { |c| c.application_name = app_name }
108
+ keys = Muchkeys::ApplicationClient.new(Muchkeys.config).known_keys
109
+
110
+ keys.join("\n")
111
+ end
112
+
113
+ def check(app_name)
114
+ Muchkeys.configure { |c| c.application_name = app_name }
115
+ Muchkeys::ApplicationClient.new(Muchkeys.config).verify_keys(*Muchkeys.env_keys)
116
+
117
+ "All keys present and accounted for."
118
+ end
119
+
120
+ def wipeout(app_name: nil)
121
+ Muchkeys.configure { |c| c.application_name = app_name }
122
+ app = Muchkeys::ApplicationClient.new
123
+ app.allow_unsafe_operation do
124
+ app.each_path do |path|
125
+ app.delete_key(path)
126
+ end
127
+ end
128
+
129
+ "Everything deleted!"
130
+ end
131
+
132
+ def delete(consul_key)
133
+ app = Muchkeys::ApplicationClient.new
134
+ app.allow_unsafe_operation do
135
+ app.delete_key(consul_key)
136
+ end
137
+
138
+ "Key #{consul_key} deleted"
44
139
  end
45
140
 
46
141
  def fetch(consul_key)
47
- MuchKeys.fetch_key(consul_key)
142
+ Muchkeys::ApplicationClient.new.fetch_key(consul_key)
48
143
  end
49
144
  end
50
145
  end
@@ -0,0 +1,51 @@
1
+ class Muchkeys::ConsulClient
2
+ delegate :config, to: :application
3
+ class SafetyViolation < StandardError; end
4
+
5
+ attr_accessor :application
6
+
7
+ def initialize(application)
8
+ @application = application
9
+ end
10
+
11
+ def unsafe=(toggle)
12
+ @unsafe = toggle
13
+ end
14
+
15
+ def get(key, recursive: false)
16
+ url = recursive ? consul_recurse_url(key) : consul_key_url(key)
17
+ response = Net::HTTP.get_response(url)
18
+
19
+ if response.code == "200"
20
+ response.body
21
+ else
22
+ nil
23
+ end
24
+ rescue Errno::ECONNREFUSED
25
+ nil
26
+ end
27
+
28
+ def put(value, key, **options)
29
+ url = consul_insert_key_url(key, **options)
30
+ raise SafetyViolation unless @unsafe
31
+ Net::HTTP.new(url.host, url.port).send_request('PUT', url.request_uri, value)
32
+ end
33
+
34
+ def delete(key)
35
+ url = consul_key_url(key)
36
+ raise SafetyViolation unless @unsafe
37
+ Net::HTTP.new(url.host, url.port).send_request('DELETE', url.request_uri)
38
+ end
39
+
40
+ def consul_key_url(key_name)
41
+ URI("#{config.consul_url}/v1/kv/#{key_name}?raw")
42
+ end
43
+
44
+ def consul_recurse_url(path)
45
+ URI("#{config.consul_url}/v1/kv/#{path}?recurse")
46
+ end
47
+
48
+ def consul_insert_key_url(key_name, **query)
49
+ URI("#{config.consul_url}/v1/kv/#{key_name}?#{query.to_query}")
50
+ end
51
+ end
@@ -1,43 +1,47 @@
1
- module MuchKeys
1
+ module Muchkeys
2
2
  class KeyValidator
3
+ attr_accessor :app_client
4
+ delegate :config, to: :app_client
3
5
 
4
- class << self
5
-
6
- # key should pass validation rules
7
- def valid? keyname
8
- exists?(keyname) &&
9
- secret_key_has_namespace?(keyname)
10
- end
6
+ def initialize(app_client)
7
+ @app_client = app_client
8
+ end
11
9
 
12
- def secret_key_namespace keyname
13
- match = keyname.match(/^secrets\/(.*?)\/.*/)
14
- if match
15
- match[1]
16
- else
17
- ""
18
- end
19
- end
20
10
 
21
- def secret_key_has_namespace? keyname
22
- if is_secret?(keyname)
23
- namespace = secret_key_namespace(keyname)
24
- exists?(namespace)
25
- else
26
- # a plain key passes, it doesn't need a namespace
27
- true
28
- end
11
+ # key should pass validation rules
12
+ def valid?(keyname)
13
+ exists?(keyname) &&
14
+ secret_key_has_namespace?(keyname)
15
+ end
16
+ alias_method :valid_key_name?, :valid?
17
+
18
+ def secret_key_namespace(keyname)
19
+ match = keyname.match(/^secrets\/(.*?)\/.*/)
20
+ if match
21
+ match[1]
22
+ else
23
+ ""
29
24
  end
25
+ end
30
26
 
31
- def is_secret? keyname
32
- keyname.match(/^secret/) != nil
27
+ def secret_key_has_namespace?(keyname)
28
+ if is_secret?(keyname)
29
+ namespace = secret_key_namespace(keyname)
30
+ exists?(namespace)
31
+ else
32
+ # a plain key passes, it doesn't need a namespace
33
+ true
33
34
  end
35
+ end
34
36
 
37
+ def is_secret?(keyname)
38
+ keyname.match(/^secret/) != nil
39
+ end
35
40
 
36
- private
37
- def exists? keyname
38
- !keyname.nil? && !keyname.empty?
39
- end
41
+ private
40
42
 
43
+ def exists?(keyname)
44
+ !keyname.nil? && !keyname.empty?
41
45
  end
42
46
  end
43
47
  end
@@ -1,19 +1,18 @@
1
- class MuchKeys::Rails < Rails::Railtie
2
- config.before_configuration do
3
- MuchKeys.configure do |config|
4
- config.application_name = Rails.application.class.parent_name.underscore
5
- end
6
-
7
- MuchKeys.populate_environment!(*env_keys)
8
- end
1
+ module Muchkeys
2
+ class Rails < Rails::Railtie
3
+ config.before_configuration do
4
+ app_config = YAML.load(File.read(::Rails.root.join("config", "muchkeys.yml")))
5
+ Muchkeys.configure do |config|
6
+ config.application_name ||= app_config[:application_name] || ::Rails.application.class.parent_name.underscore
7
+ config.consul_url ||= app_config[:consul_url] || "http://localhost:8500"
8
+ config.keys_dir ||= app_config[:keys_dir]
9
+ config.private_key ||= app_config[:private_key]
10
+ config.public_key ||= app_config[:public_key]
11
+ config.search_paths ||= app_config[:search_paths]
12
+ config.secrets_hint ||= app_config[:secrets_hint]
13
+ end
9
14
 
10
-
11
- def env_keys
12
- # parse all environments found in .env and populate them from consul
13
- unless File.exists?(Rails.root.join(".env"))
14
- raise IOError, ".env files are required for Muchkeys ENV injection to work"
15
+ Muchkeys.populate_environment!(*Muchkeys.env_keys)
15
16
  end
16
-
17
- File.read(Rails.root.join(".env")).each_line.map { |x| x.split("=")[0] }
18
17
  end
19
18
  end
@@ -1,65 +1,69 @@
1
+ require 'active_support/core_ext/module/delegation'
1
2
  require 'openssl'
2
3
  require 'net/http'
3
- require 'muchkeys/configuration'
4
- require 'muchkeys/errors'
5
4
 
6
-
7
- class MuchKeys::Secret
5
+ class Muchkeys::Secret
8
6
  CIPHER_SUITE = "AES-256-CFB"
9
7
 
10
- class << self
8
+ attr_accessor :app_client
9
+
10
+ delegate :config, to: :app_client
11
11
 
12
- # the path that clues MuchKeys that this path contains secrets
13
- def secrets_path_hint
14
- MuchKeys.configuration.secrets_hint || "secrets/"
15
- end
12
+ def initialize(app_client)
13
+ @app_client = app_client
14
+ end
16
15
 
17
- def encrypt_string(val, public_key)
18
- cipher = OpenSSL::Cipher.new CIPHER_SUITE
19
- cert = OpenSSL::X509::Certificate.new File.read(public_key)
20
- OpenSSL::PKCS7::encrypt([cert], val, cipher, OpenSSL::PKCS7::BINARY)
21
- end
16
+ # the path that clues Muchkeys that this path contains secrets
17
+ def secrets_path_hint
18
+ config.secrets_hint || "secrets"
19
+ end
22
20
 
23
- # turn a key_name into a SSL cert file name by convention
24
- def certfile_name(key_name)
25
- key_parts = key_name.match /(.*)\/#{secrets_path_hint}(.*)/
26
- raise MuchKeys::InvalidKey, "#{key_name} doesn't look like a secret" if key_parts.nil?
27
- key_base = key_parts[1].gsub(/^git\//, "")
28
- MuchKeys.configuration.public_key || "#{ENV['HOME']}/.keys/#{key_base}.pem"
29
- end
21
+ def encrypt_string(val, public_key)
22
+ cipher = OpenSSL::Cipher.new CIPHER_SUITE
23
+ cert = OpenSSL::X509::Certificate.new File.read(public_key)
24
+ OpenSSL::PKCS7::encrypt([cert], val, cipher, OpenSSL::PKCS7::BINARY)
25
+ end
30
26
 
31
- def is_secret?(key_name)
32
- key_name.match(/\/#{secrets_path_hint}/) != nil
33
- end
27
+ # turn a key_name into a SSL cert file name by convention
28
+ def certfile_name(key_name)
29
+ key_parts = key_name.match /(.*)\/#{secrets_path_hint}(.*)/
30
+ # FIXME this already checked in the secretes validator, we don't need to
31
+ # check it again
32
+ raise Muchkeys::InvalidKey, "#{key_name} doesn't look like a secret" if key_parts.nil?
33
+ key_base = key_parts[1].gsub(/^git\//, "")
34
+ config.public_key || "#{ENV['HOME']}/.keys/#{key_base}.pem"
35
+ end
34
36
 
35
- def auto_certificates_exist_for_key?(key)
36
- file_exists?(secret_adapter.certfile_name(key))
37
- end
37
+ def is_secret?(key_name)
38
+ key_name.match(/\/#{secrets_path_hint}/) != nil
39
+ end
38
40
 
39
- def decrypt_string(val, public_key, private_key)
40
- cert = OpenSSL::X509::Certificate.new(read_ssl_key(public_key))
41
- key = OpenSSL::PKey::RSA.new(read_ssl_key(private_key))
42
- OpenSSL::PKCS7.new(val).decrypt(key, cert)
43
- end
41
+ def auto_certificates_exist_for_key?(key)
42
+ file_exists?(certfile_name(key))
43
+ end
44
44
 
45
- private
45
+ def decrypt_string(val, public_key = nil, private_key = nil)
46
+ cert = OpenSSL::X509::Certificate.new(read_ssl_key(public_key))
47
+ key = OpenSSL::PKey::RSA.new(read_ssl_key(private_key))
48
+ OpenSSL::PKCS7.new(val).decrypt(key, cert)
49
+ end
46
50
 
47
- def read_ssl_key(file_name)
48
- File.read file_name
49
- end
51
+ private
50
52
 
51
- # Why would we even do this? For stubbing.
52
- def file_exists?(path)
53
- File.exist? path
54
- end
53
+ def read_ssl_key(file_name)
54
+ File.read(file_name)
55
+ end
55
56
 
56
- def key_validator
57
- MuchKeys::KeyValidator
58
- end
57
+ # Why would we even do this? For stubbing.
58
+ def file_exists?(path)
59
+ File.exist?(path)
60
+ end
59
61
 
60
- def secret_adapter
61
- MuchKeys::Secret
62
- end
62
+ def key_validator
63
+ Muchkeys::KeyValidator
64
+ end
63
65
 
66
+ def secret_adapter
67
+ Muchkeys::Secret
64
68
  end
65
69
  end
@@ -1,3 +1,3 @@
1
- module MuchKeys
2
- VERSION = "0.5.0"
1
+ module Muchkeys
2
+ VERSION = "0.7.0"
3
3
  end
data/lib/muchkeys.rb CHANGED
@@ -1,175 +1,67 @@
1
- require "muchkeys/version"
2
- require "muchkeys/errors"
3
- require "muchkeys/configuration"
4
- require "muchkeys/secret"
5
- require "muchkeys/key_validator"
6
- require "muchkeys/cli"
7
-
1
+ require "active_support/configurable"
8
2
  require "net/http"
3
+ require "json"
9
4
 
10
- if defined? Rails
5
+ if defined? ::Rails
11
6
  require 'muchkeys/rails'
12
7
  end
13
8
 
14
- module MuchKeys
15
-
16
- # revealing intention
17
- module BlankKey; false; end
18
-
19
-
20
- class << self
21
- attr_accessor :configuration
22
-
23
- def configure
24
- self.configuration ||= MuchKeys::Configuration.new
25
- if block_given?
26
- yield configuration
27
- end
28
- end
29
-
30
- def populate_environment!(*keys)
31
- keys.each do |key|
32
- ENV[key] = MuchKeys.find_first(key)
33
- end
34
- nil
35
- end
36
-
37
- # this is the entry point to the ENV-like object that devs will use
38
- def find_first(key_name)
39
- unless MuchKeys.configuration.search_paths
40
- raise MuchKeys::UnknownApplication, "Can't detect app name and application_name isn't set." if !application_name
41
- end
42
-
43
- consul_paths_to_search = search_order(application_name, key_name)
44
-
45
- # search consul in a specific order until we find something
46
- response = nil
47
- consul_paths_to_search.detect do |consul_path|
48
- response = fetch_key(consul_path)
49
- response if response != MuchKeys::BlankKey
50
- end
51
-
52
- if response == BlankKey
53
- raise MuchKeys::NoKeysSet, "Bailing. Consul isn't set with any keys for #{key_name}."
54
- end
55
-
56
- response
57
- end
58
-
59
- def fetch_key(key_name, public_key:nil, private_key:nil)
60
- return ENV[key_name] unless ENV[key_name].nil?
61
-
62
- if secret_adapter.is_secret?(key_name)
63
- raise InvalidKey unless validate_key_name(key_name)
64
-
65
- # configure automatic certificates
66
- public_key = find_certfile_for_key(key_name) if public_key.nil?
67
- private_key = find_certfile_for_key(key_name) if private_key.nil?
68
-
69
- response = fetch_secret_key(key_name, public_key, private_key)
70
- else
71
- response = fetch_plain_key(key_name)
72
- end
73
-
74
- # otherwise, we got consul data so return that
75
- response
76
- end
77
-
78
- def search_order(application_name, key_name)
79
- if MuchKeys.configuration.search_paths
80
- search_paths = MuchKeys.configuration.search_paths.collect do |path|
81
- "#{path}/#{key_name}"
82
- end
83
- else
84
- search_paths = [
85
- "git/#{application_name}/secrets/#{key_name}",
86
- "git/#{application_name}/config/#{key_name}",
87
- "git/shared/secrets/#{key_name}",
88
- "git/shared/config/#{key_name}"
89
- ]
90
- end
9
+ module Muchkeys
10
+ class InvalidKey < StandardError; end
11
+ class CLIOptionsError < StandardError; end
12
+ class KeyNotSet < StandardError; end
13
+ class UnknownApplication < StandardError; end
14
+ class AmbigousPath < StandardError; end
91
15
 
92
- search_paths
93
- end
94
-
95
-
96
- private
97
- def consul_url(key_name)
98
- URI("#{configuration.consul_url}/v1/kv/#{key_name}?raw")
99
- end
100
-
101
- def fetch_plain_key(key_name)
102
- url = consul_url(key_name)
103
- begin
104
- response = Net::HTTP.get_response url
16
+ autoload :KeyValidator, 'muchkeys/key_validator'
17
+ autoload :ApplicationClient, 'muchkeys/application_client'
18
+ autoload :Secret, 'muchkeys/secret'
19
+ autoload :CLI, 'muchkeys/cli'
20
+ autoload :ConsulClient, 'muchkeys/consul_client'
105
21
 
106
- return MuchKeys::BlankKey if !response.body || response.body.empty?
107
- response.body
108
- rescue Errno::ECONNREFUSED
109
- return nil
110
- end
111
- end
22
+ include ActiveSupport::Configurable
112
23
 
113
- def fetch_secret_key(key_name, public_pem=nil, private_pem=nil)
114
- result = fetch_plain_key(key_name)
24
+ config_accessor :application_name
115
25
 
116
- # we hit a key that doesn't exist, so don't try to decrypt it
117
- return MuchKeys::BlankKey if result == MuchKeys::BlankKey || result.nil?
118
-
119
- secret_adapter.decrypt_string(result, public_pem, private_pem)
120
- end
121
-
122
- def find_certfile_for_key(key_name)
123
- secret_adapter.certfile_name key_name
124
- end
26
+ config_accessor :consul_url do
27
+ 'http://localhost:8500'
28
+ end
125
29
 
126
- def secret_adapter
127
- MuchKeys::Secret
128
- end
30
+ config_accessor :private_key
31
+ config_accessor :public_key
32
+ config_accessor :application_name
33
+ config_accessor :secrets_hint
34
+ config_accessor :search_paths do
35
+ %w(git/%{application_name}/secrets
36
+ git/%{application_name}/config
37
+ git/shared/secrets
38
+ git/shared/config)
39
+ end
129
40
 
130
- def validate_key_name(key_name)
131
- MuchKeys::KeyValidator.valid? key_name
41
+ def self.env_keys
42
+ # parse all environments found in .env and populate them from consul
43
+ if defined? ::Rails
44
+ check_dir = ::Rails.root
45
+ else
46
+ check_dir = Pathname.getwd
132
47
  end
133
48
 
134
- # Detecting Rails app names is a known quantity.
135
- # Rack apps need to set the name through config.
136
- def application_name
137
- return configuration.application_name if configuration.application_name
138
-
139
- if defined?(Rails)
140
- # Rails.application looks something like "Monorail::Application"
141
- application_name = Rails.application.class.to_s.split("::").first
142
- else
143
- return false
144
- end
145
-
146
- snakecase(application_name)
147
- end
148
49
 
149
- # MyApp should become my_app
150
- def snakecase(string)
151
- string.gsub(/::/, '/').
152
- gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
153
- gsub(/([a-z\d])([A-Z])/,'\1_\2').
154
- tr("-", "_").
155
- downcase
50
+ unless File.exists?(check_dir.join(".env"))
51
+ raise IOError, ".env files are required for Muchkeys ENV injection to work"
156
52
  end
157
53
 
54
+ File.read(check_dir.join(".env")).each_line.select(&:presence).map { |x| x.split("=")[0] }
158
55
  end
159
- end
160
-
161
- # default configure the gem on gem load
162
- MuchKeys.configuration ||= MuchKeys::Configuration.new
163
-
164
56
 
165
- # This interface looks like ENV which is more friendly to devs
166
- class MUCHKEYS
57
+ def self.populate_environment!(*required_keys)
58
+ app = ApplicationClient.new(config)
59
+ app.verify_keys(*required_keys)
167
60
 
168
- def self.[](key_name)
169
- # if an ENV key is set, use that first
170
- return ENV[key_name] unless ENV[key_name].nil?
61
+ app.known_keys.each do |key|
62
+ ENV[key] ||= app.first(key)
63
+ end
171
64
 
172
- MuchKeys.find_first(key_name)
65
+ nil
173
66
  end
174
-
175
67
  end
data/muchkeys.gemspec CHANGED
@@ -5,12 +5,12 @@ require 'muchkeys/version'
5
5
 
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = "muchkeys"
8
- spec.version = MuchKeys::VERSION
8
+ spec.version = Muchkeys::VERSION
9
9
  spec.authors = ["Pat O'Brien", "Chris Dillon"]
10
10
  spec.email = ["pobrien@goldstar.com", "cdillon@goldstar.com"]
11
11
 
12
- spec.summary = %q{MuchKeys fetches keys from the ENV and then falls back to consul}
13
- spec.description = %q{MuchKeys can handle app configuration and appsecrets}
12
+ spec.summary = %q{Muchkeys fetches keys from the ENV and then falls back to consul}
13
+ spec.description = %q{Muchkeys can handle app configuration and appsecrets}
14
14
  spec.homepage = "https://www.goldstar.com"
15
15
  spec.license = "MIT"
16
16
 
@@ -20,11 +20,10 @@ Gem::Specification.new do |spec|
20
20
  spec.require_paths = ["lib"]
21
21
 
22
22
  spec.add_runtime_dependency "thor"
23
+ spec.add_runtime_dependency "activesupport", "< 5.1"
23
24
 
24
25
  spec.add_development_dependency "bundler", "~> 1.10"
25
26
  spec.add_development_dependency "rake", "~> 10.0"
26
27
  spec.add_development_dependency "rspec", "~> 3.3"
27
- spec.add_development_dependency "webmock", "~> 1.20"
28
- spec.add_development_dependency "vcr", "~> 2.9"
29
28
  spec.add_development_dependency "aruba", "~> 0.14.2"
30
29
  end
data/secret.txt ADDED
@@ -0,0 +1 @@
1
+ it's a seekwet
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: muchkeys
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pat O'Brien
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2016-09-28 00:00:00.000000000 Z
12
+ date: 2016-12-20 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: thor
@@ -25,6 +25,20 @@ dependencies:
25
25
  - - ">="
26
26
  - !ruby/object:Gem::Version
27
27
  version: '0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: activesupport
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "<"
33
+ - !ruby/object:Gem::Version
34
+ version: '5.1'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '5.1'
28
42
  - !ruby/object:Gem::Dependency
29
43
  name: bundler
30
44
  requirement: !ruby/object:Gem::Requirement
@@ -67,34 +81,6 @@ dependencies:
67
81
  - - "~>"
68
82
  - !ruby/object:Gem::Version
69
83
  version: '3.3'
70
- - !ruby/object:Gem::Dependency
71
- name: webmock
72
- requirement: !ruby/object:Gem::Requirement
73
- requirements:
74
- - - "~>"
75
- - !ruby/object:Gem::Version
76
- version: '1.20'
77
- type: :development
78
- prerelease: false
79
- version_requirements: !ruby/object:Gem::Requirement
80
- requirements:
81
- - - "~>"
82
- - !ruby/object:Gem::Version
83
- version: '1.20'
84
- - !ruby/object:Gem::Dependency
85
- name: vcr
86
- requirement: !ruby/object:Gem::Requirement
87
- requirements:
88
- - - "~>"
89
- - !ruby/object:Gem::Version
90
- version: '2.9'
91
- type: :development
92
- prerelease: false
93
- version_requirements: !ruby/object:Gem::Requirement
94
- requirements:
95
- - - "~>"
96
- - !ruby/object:Gem::Version
97
- version: '2.9'
98
84
  - !ruby/object:Gem::Dependency
99
85
  name: aruba
100
86
  requirement: !ruby/object:Gem::Requirement
@@ -109,7 +95,7 @@ dependencies:
109
95
  - - "~>"
110
96
  - !ruby/object:Gem::Version
111
97
  version: 0.14.2
112
- description: MuchKeys can handle app configuration and appsecrets
98
+ description: Muchkeys can handle app configuration and appsecrets
113
99
  email:
114
100
  - pobrien@goldstar.com
115
101
  - cdillon@goldstar.com
@@ -122,22 +108,25 @@ files:
122
108
  - ".rspec"
123
109
  - ".ruby-version"
124
110
  - ".travis.yml"
111
+ - Dockerfile
125
112
  - Gemfile
126
113
  - Guardfile
127
114
  - LICENSE.txt
128
115
  - README.md
129
116
  - Rakefile
130
117
  - circle.yml
118
+ - docker-compose.yml
131
119
  - exe/muchkeys
132
120
  - lib/muchkeys.rb
121
+ - lib/muchkeys/application_client.rb
133
122
  - lib/muchkeys/cli.rb
134
- - lib/muchkeys/configuration.rb
135
- - lib/muchkeys/errors.rb
123
+ - lib/muchkeys/consul_client.rb
136
124
  - lib/muchkeys/key_validator.rb
137
125
  - lib/muchkeys/rails.rb
138
126
  - lib/muchkeys/secret.rb
139
127
  - lib/muchkeys/version.rb
140
128
  - muchkeys.gemspec
129
+ - secret.txt
141
130
  homepage: https://www.goldstar.com
142
131
  licenses:
143
132
  - MIT
@@ -161,5 +150,5 @@ rubyforge_project:
161
150
  rubygems_version: 2.5.1
162
151
  signing_key:
163
152
  specification_version: 4
164
- summary: MuchKeys fetches keys from the ENV and then falls back to consul
153
+ summary: Muchkeys fetches keys from the ENV and then falls back to consul
165
154
  test_files: []
@@ -1,30 +0,0 @@
1
- require 'muchkeys'
2
- require 'uri'
3
-
4
- module MuchKeys
5
- class Configuration
6
- attr_accessor :consul_url, :private_key, :public_key, :application_name, :search_paths, :secrets_hint
7
-
8
- # sensible defaults
9
- def initialize
10
- @consul_url = "http://localhost:8500"
11
- end
12
-
13
- # url parsing sanity check
14
- def consul_url=(url)
15
- raise URI::InvalidURIError unless url =~ URI::regexp
16
- @consul_url = url
17
- end
18
-
19
- def attributes
20
- {
21
- consul_url: @consul_url,
22
- private_key: @private_key,
23
- public_key: @public_key,
24
- application_name: @application_name,
25
- search_paths: @search_paths,
26
- secrets_hint: @secrets_hint
27
- }.delete_if {|k,v| v.nil? }
28
- end
29
- end
30
- end
@@ -1,6 +0,0 @@
1
- module MuchKeys
2
- InvalidKey = Class.new(StandardError)
3
- CLIOptionsError = Class.new(StandardError)
4
- NoKeysSet = Class.new(StandardError)
5
- UnknownApplication = Class.new(StandardError)
6
- end