vault 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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