vault 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +41 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +6 -0
  5. data/CHANGELOG.md +5 -0
  6. data/Gemfile +3 -0
  7. data/Gemfile.lock +32 -0
  8. data/LICENSE +362 -0
  9. data/README.md +80 -54
  10. data/Rakefile +4 -40
  11. data/lib/vault.rb +33 -46
  12. data/lib/vault/api.rb +9 -0
  13. data/lib/vault/api/auth_token.rb +90 -0
  14. data/lib/vault/api/help.rb +23 -0
  15. data/lib/vault/api/logical.rb +66 -0
  16. data/lib/vault/api/secret.rb +23 -0
  17. data/lib/vault/api/sys.rb +24 -0
  18. data/lib/vault/api/sys/audit.rb +60 -0
  19. data/lib/vault/api/sys/auth.rb +58 -0
  20. data/lib/vault/api/sys/init.rb +46 -0
  21. data/lib/vault/api/sys/leader.rb +25 -0
  22. data/lib/vault/api/sys/lease.rb +51 -0
  23. data/lib/vault/api/sys/mount.rb +75 -0
  24. data/lib/vault/api/sys/policy.rb +76 -0
  25. data/lib/vault/api/sys/seal.rb +49 -0
  26. data/lib/vault/client.rb +285 -0
  27. data/lib/vault/configurable.rb +48 -0
  28. data/lib/vault/defaults.rb +68 -0
  29. data/lib/vault/errors.rb +48 -0
  30. data/lib/vault/request.rb +19 -0
  31. data/lib/vault/response.rb +20 -0
  32. data/lib/vault/version.rb +1 -6
  33. data/vault.gemspec +25 -0
  34. metadata +97 -98
  35. data/MIT-LICENSE +0 -20
  36. data/lib/vault/associations.rb +0 -39
  37. data/lib/vault/attribute_accessors.rb +0 -29
  38. data/lib/vault/bulk_attributes.rb +0 -17
  39. data/lib/vault/dirty.rb +0 -37
  40. data/lib/vault/finders.rb +0 -24
  41. data/lib/vault/persistance.rb +0 -47
  42. data/lib/vault/properties.rb +0 -68
  43. data/lib/vault/scoping.rb +0 -64
  44. data/lib/vault/storage.rb +0 -4
  45. data/lib/vault/storage/in_memory_store.rb +0 -14
  46. data/lib/vault/storage/yaml_store.rb +0 -52
  47. data/lib/vault/validations.rb +0 -13
  48. data/spec/active_model_compliance_spec.rb +0 -33
  49. data/spec/spec_helper.rb +0 -8
  50. data/spec/support/helpers.rb +0 -16
  51. data/spec/support/storage_api.rb +0 -14
  52. data/spec/vault/associations_spec.rb +0 -73
  53. data/spec/vault/finders_spec.rb +0 -69
  54. data/spec/vault/persistance_spec.rb +0 -126
  55. data/spec/vault/properties_spec.rb +0 -59
  56. data/spec/vault/scoping_spec.rb +0 -53
  57. data/spec/vault/storage/in_memory_store_spec.rb +0 -5
  58. data/spec/vault/storage/yaml_store_spec.rb +0 -29
  59. data/spec/vault_spec.rb +0 -33
@@ -0,0 +1,49 @@
1
+ require "json"
2
+
3
+ require_relative "../sys"
4
+
5
+ module Vault
6
+ class SealStatus < Response.new(:sealed, :t, :n, :progress)
7
+ alias_method :sealed?, :sealed
8
+ end
9
+
10
+ class Sys
11
+ # Get the current seal status.
12
+ #
13
+ # @example
14
+ # Vault.sys.seal_status #=> #<Vault::SealStatus sealed=false, t=1, n=1, progress=0>
15
+ #
16
+ # @return [SealStatus]
17
+ def seal_status
18
+ json = client.get("/v1/sys/seal-status")
19
+ return SealStatus.decode(json)
20
+ end
21
+
22
+ # Seal the vault. Warning: this will seal the vault!
23
+ #
24
+ # @example
25
+ # Vault.sys.seal #=> true
26
+ #
27
+ # @return [true]
28
+ def seal
29
+ client.put("/v1/sys/seal", nil)
30
+ return true
31
+ end
32
+
33
+ # Unseal the vault with the given shard.
34
+ #
35
+ # @example
36
+ # Vault.sys.unseal("abcd-1234") #=> #<Vault::SealStatus sealed=true, t=3, n=5, progress=1>
37
+ #
38
+ # @param [String] shard
39
+ # the key to use
40
+ #
41
+ # @return [SealStatus]
42
+ def unseal(shard)
43
+ json = client.put("/v1/sys/unseal", JSON.fast_generate(
44
+ key: shard,
45
+ ))
46
+ return SealStatus.decode(json)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,285 @@
1
+ require "cgi"
2
+ require "cgi/cookie"
3
+ require "json"
4
+ require "net/http"
5
+ require "net/https"
6
+ require "uri"
7
+
8
+ require_relative "configurable"
9
+ require_relative "errors"
10
+
11
+ module Vault
12
+ class Client
13
+ # The user agent for this client.
14
+ USER_AGENT = "VaultRuby/#{Vault::VERSION} (+github.com/hashicorp/vault-ruby)".freeze
15
+
16
+ # The default headers that are sent with every request.
17
+ DEFAULT_HEADERS = {
18
+ "Content-Type" => "application/json",
19
+ "Accept" => "application/json",
20
+ "User-Agent" => USER_AGENT,
21
+ }.freeze
22
+
23
+ # The default list of options to use when parsing JSON.
24
+ JSON_PARSE_OPTIONS = {
25
+ max_nesting: false,
26
+ create_additions: false,
27
+ symbolize_names: true,
28
+ }.freeze
29
+
30
+ include Vault::Configurable
31
+
32
+ # Create a new Client with the given options. Any options given take
33
+ # precedence over the default options.
34
+ #
35
+ # @return [Vault::Client]
36
+ def initialize(options = {})
37
+ # Use any options given, but fall back to the defaults set on the module
38
+ Vault::Configurable.keys.each do |key|
39
+ value = if options[key].nil?
40
+ Vault.instance_variable_get(:"@#{key}")
41
+ else
42
+ options[key]
43
+ end
44
+
45
+ instance_variable_set(:"@#{key}", value)
46
+ end
47
+ end
48
+
49
+ # Determine if the given options are the same as ours.
50
+ # @return [true, false]
51
+ def same_options?(opts)
52
+ options.hash == opts.hash
53
+ end
54
+
55
+ # Perform a GET request.
56
+ # @see Client#request
57
+ def get(path, params = {}, headers = {})
58
+ request(:get, path, params, headers)
59
+ end
60
+
61
+ # Perform a POST request.
62
+ # @see Client#request
63
+ def post(path, data, headers = {})
64
+ request(:post, path, data, headers)
65
+ end
66
+
67
+ # Perform a PUT request.
68
+ # @see Client#request
69
+ def put(path, data, headers = {})
70
+ request(:put, path, data, headers)
71
+ end
72
+
73
+ # Perform a PATCH request.
74
+ # @see Client#request
75
+ def patch(path, data, headers = {})
76
+ request(:patch, path, data, headers)
77
+ end
78
+
79
+ # Perform a DELETE request.
80
+ # @see Client#request
81
+ def delete(path, params = {}, headers = {})
82
+ request(:delete, path, params, headers)
83
+ end
84
+
85
+ # Make an HTTP request with the given verb, data, params, and headers. If
86
+ # the response has a return type of JSON, the JSON is automatically parsed
87
+ # and returned as a hash; otherwise it is returned as a string.
88
+ #
89
+ # @raise [HTTPError]
90
+ # if the request is not an HTTP 200 OK
91
+ #
92
+ # @param [Symbol] verb
93
+ # the lowercase symbol of the HTTP verb (e.g. :get, :delete)
94
+ # @param [String] path
95
+ # the absolute or relative path from {Defaults.address} to make the
96
+ # request against
97
+ # @param [#read, Hash, nil] data
98
+ # the data to use (varies based on the +verb+)
99
+ # @param [Hash] headers
100
+ # the list of headers to use
101
+ #
102
+ # @return [String, Hash]
103
+ # the response body
104
+ def request(verb, path, data = {}, headers = {})
105
+ # All requests to vault require a token, so we should error without even
106
+ # trying if there is no token set
107
+ raise MissingTokenError if token.nil?
108
+
109
+ # Build the URI and request object from the given information
110
+ uri = build_uri(verb, path, data)
111
+ request = class_for_request(verb).new(uri.request_uri)
112
+
113
+ # Add headers
114
+ headers = DEFAULT_HEADERS.merge(headers)
115
+ headers.each do |key, value|
116
+ request.add_field(key, value)
117
+ end
118
+
119
+ # Setup PATCH/POST/PUT
120
+ if [:patch, :post, :put].include?(verb)
121
+ if data.respond_to?(:read)
122
+ request.content_length = data.size
123
+ request.body_stream = data
124
+ elsif data.is_a?(Hash)
125
+ request.form_data = data
126
+ else
127
+ request.body = data
128
+ end
129
+ end
130
+
131
+ # Create the HTTP connection object - since the proxy information defaults
132
+ # to +nil+, we can just pass it to the initializer method instead of doing
133
+ # crazy strange conditionals.
134
+ connection = Net::HTTP.new(uri.host, uri.port,
135
+ proxy_address, proxy_port, proxy_username, proxy_password)
136
+
137
+ # Create the cookie for the request.
138
+ cookie = CGI::Cookie.new
139
+ cookie.name = "token"
140
+ cookie.value = token
141
+ cookie.path = "/"
142
+ cookie.expires = Time.now + (60*60*24*376)
143
+
144
+ # Apply SSL, if applicable
145
+ if uri.scheme == "https"
146
+ # Turn on SSL
147
+ connection.use_ssl = true
148
+
149
+ # Turn on secure cookies
150
+ cookie.secure = true
151
+
152
+ # Custom pem files, no problem!
153
+ if ssl_pem_file
154
+ pem = File.read(ssl_pem_file)
155
+ connection.cert = OpenSSL::X509::Certificate.new(pem)
156
+ connection.key = OpenSSL::PKey::RSA.new(pem)
157
+ connection.verify_mode = OpenSSL::SSL::VERIFY_PEER
158
+ end
159
+
160
+ # Naughty, naughty, naughty! Don't blame when when someone hops in
161
+ # and executes a MITM attack!
162
+ unless ssl_verify
163
+ connection.verify_mode = OpenSSL::SSL::VERIFY_NONE
164
+ end
165
+ end
166
+
167
+ # Add the cookie to the request.
168
+ request["Cookie"] = cookie.to_s
169
+
170
+ # Create a connection using the block form, which will ensure the socket
171
+ # is properly closed in the event of an error.
172
+ connection.start do |http|
173
+ response = http.request(request)
174
+
175
+ case response
176
+ when Net::HTTPRedirection
177
+ redirect = URI.parse(response["location"])
178
+ request(verb, redirect, data, headers)
179
+ when Net::HTTPSuccess
180
+ success(response)
181
+ else
182
+ error(response)
183
+ end
184
+ end
185
+ rescue SocketError, Errno::ECONNREFUSED, EOFError
186
+ raise HTTPConnectionError.new(address)
187
+ end
188
+
189
+ # Construct a URL from the given verb and path. If the request is a GET or
190
+ # DELETE request, the params are assumed to be query params are are
191
+ # converted as such using {Client#to_query_string}.
192
+ #
193
+ # If the path is relative, it is merged with the {Defaults.address}
194
+ # attribute. If the path is absolute, it is converted to a URI object and
195
+ # returned.
196
+ #
197
+ # @param [Symbol] verb
198
+ # the lowercase HTTP verb (e.g. :+get+)
199
+ # @param [String] path
200
+ # the absolute or relative HTTP path (url) to get
201
+ # @param [Hash] params
202
+ # the list of params to build the URI with (for GET and DELETE requests)
203
+ #
204
+ # @return [URI]
205
+ def build_uri(verb, path, params = {})
206
+ # Add any query string parameters
207
+ if [:delete, :get].include?(verb)
208
+ path = [path, to_query_string(params)].compact.join("?")
209
+ end
210
+
211
+ # Parse the URI
212
+ uri = URI.parse(path)
213
+
214
+ # Don't merge absolute URLs
215
+ uri = URI.parse(File.join(address, path)) unless uri.absolute?
216
+
217
+ # Return the URI object
218
+ uri
219
+ end
220
+
221
+ # Helper method to get the corresponding {Net::HTTP} class from the given
222
+ # HTTP verb.
223
+ #
224
+ # @param [#to_s] verb
225
+ # the HTTP verb to create a class from
226
+ #
227
+ # @return [Class]
228
+ def class_for_request(verb)
229
+ Net::HTTP.const_get(verb.to_s.capitalize)
230
+ end
231
+
232
+ # Convert the given hash to a list of query string parameters. Each key and
233
+ # value in the hash is URI-escaped for safety.
234
+ #
235
+ # @param [Hash] hash
236
+ # the hash to create the query string from
237
+ #
238
+ # @return [String, nil]
239
+ # the query string as a string, or +nil+ if there are no params
240
+ def to_query_string(hash)
241
+ hash.map do |key, value|
242
+ "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"
243
+ end.join('&')[/.+/]
244
+ end
245
+
246
+ # Parse the response object and manipulate the result based on the given
247
+ # +Content-Type+ header. For now, this method only parses JSON, but it
248
+ # could be expanded in the future to accept other content types.
249
+ #
250
+ # @param [HTTP::Message] response
251
+ # the response object from the request
252
+ #
253
+ # @return [String, Hash]
254
+ # the parsed response, as an object
255
+ def success(response)
256
+ if response.body && (response.content_type || '').include?("json")
257
+ JSON.parse(response.body, JSON_PARSE_OPTIONS)
258
+ else
259
+ response.body
260
+ end
261
+ end
262
+
263
+ # Raise a response error, extracting as much information from the server's
264
+ # response as possible.
265
+ #
266
+ # @raise [HTTPError]
267
+ #
268
+ # @param [HTTP::Message] response
269
+ # the response object from the request
270
+ def error(response)
271
+ if (response.content_type || '').include?("json")
272
+ # Attempt to parse the error as JSON
273
+ begin
274
+ json = JSON.parse(response.body, JSON_PARSE_OPTIONS)
275
+
276
+ if json[:errors]
277
+ raise HTTPError.new(address, response.code, json[:errors])
278
+ end
279
+ rescue JSON::ParserError; end
280
+ end
281
+
282
+ raise HTTPError.new(address, response.code, [response.body])
283
+ end
284
+ end
285
+ end
@@ -0,0 +1,48 @@
1
+ require_relative "defaults"
2
+
3
+ module Vault
4
+ module Configurable
5
+ def self.keys
6
+ @keys ||= [
7
+ :address,
8
+ :token,
9
+ :proxy_address,
10
+ :proxy_password,
11
+ :proxy_port,
12
+ :proxy_username,
13
+ :ssl_pem_file,
14
+ :ssl_verify,
15
+ ]
16
+ end
17
+
18
+ Vault::Configurable.keys.each(&method(:attr_accessor))
19
+
20
+ # Configure yields self for block-style configuration.
21
+ #
22
+ # @yield [self]
23
+ def configure
24
+ yield self
25
+ end
26
+
27
+ # Reset all the values to their defaults.
28
+ #
29
+ # @return [self]
30
+ def reset!
31
+ defaults = Defaults.options
32
+ Vault::Configurable.keys.each do |key|
33
+ instance_variable_set(:"@#{key}", defaults[key])
34
+ end
35
+ self
36
+ end
37
+ alias_method :setup!, :reset!
38
+
39
+ # The list of options for this configurable.
40
+ #
41
+ # @return [Hash<Symbol, Object>]
42
+ def options
43
+ Hash[*Vault::Configurable.keys.map do |key|
44
+ [key, instance_variable_get(:"@#{key}")]
45
+ end.flatten]
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,68 @@
1
+ module Vault
2
+ module Defaults
3
+ # The default vault address.
4
+ # @return [String]
5
+ VAULT_ADDRESS = "https://127.0.0.1:8200".freeze
6
+
7
+ class << self
8
+ # The list of calculated options for this configurable.
9
+ # @return [Hash]
10
+ def options
11
+ Hash[*Configurable.keys.map { |key| [key, public_send(key)] }.flatten]
12
+ end
13
+
14
+ # The address to communicate with Vault.
15
+ # @return [String]
16
+ def address
17
+ ENV["VAULT_ADDR"] || VAULT_ADDRESS
18
+ end
19
+
20
+ # The vault token to use for authentiation.
21
+ # @return [String, nil]
22
+ def token
23
+ ENV["VAULT_TOKEN"]
24
+ end
25
+
26
+ # The HTTP Proxy server address as a string
27
+ # @return [String, nil]
28
+ def proxy_address
29
+ ENV["VAULT_PROXY_ADDRESS"]
30
+ end
31
+
32
+ # The HTTP Proxy server username as a string
33
+ # @return [String, nil]
34
+ def proxy_username
35
+ ENV["VAULT_PROXY_USERNAME"]
36
+ end
37
+
38
+ # The HTTP Proxy user password as a string
39
+ # @return [String, nil]
40
+ def proxy_password
41
+ ENV["VAULT_PROXY_PASSWORD"]
42
+ end
43
+
44
+ # The HTTP Proxy server port as a string
45
+ # @return [String, nil]
46
+ def proxy_port
47
+ ENV["VAULT_PROXY_PORT"]
48
+ end
49
+
50
+ # The path to a pem on disk to use with custom SSL verification
51
+ # @return [String, nil]
52
+ def ssl_pem_file
53
+ ENV["VAULT_SSL_CERT"]
54
+ end
55
+
56
+ # Verify SSL requests (default: true)
57
+ #
58
+ # @return [true, false]
59
+ def ssl_verify
60
+ if ENV["VAULT_SSL_VERIFY"].nil?
61
+ true
62
+ else
63
+ %w[t y].include?(ENV["VAULT_SSL_VERIFY"].downcase[0])
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end