vault_ruby_client 0.18.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +287 -0
  3. data/LICENSE +364 -0
  4. data/README.md +223 -0
  5. data/lib/vault/api/approle.rb +221 -0
  6. data/lib/vault/api/auth.rb +324 -0
  7. data/lib/vault/api/auth_tls.rb +95 -0
  8. data/lib/vault/api/auth_token.rb +245 -0
  9. data/lib/vault/api/help.rb +36 -0
  10. data/lib/vault/api/kv.rb +230 -0
  11. data/lib/vault/api/logical.rb +153 -0
  12. data/lib/vault/api/secret.rb +171 -0
  13. data/lib/vault/api/sys/audit.rb +94 -0
  14. data/lib/vault/api/sys/auth.rb +119 -0
  15. data/lib/vault/api/sys/health.rb +66 -0
  16. data/lib/vault/api/sys/init.rb +86 -0
  17. data/lib/vault/api/sys/leader.rb +51 -0
  18. data/lib/vault/api/sys/lease.rb +52 -0
  19. data/lib/vault/api/sys/mount.rb +165 -0
  20. data/lib/vault/api/sys/namespace.rb +86 -0
  21. data/lib/vault/api/sys/policy.rb +95 -0
  22. data/lib/vault/api/sys/quota.rb +110 -0
  23. data/lib/vault/api/sys/seal.rb +84 -0
  24. data/lib/vault/api/sys.rb +30 -0
  25. data/lib/vault/api/transform/alphabet.rb +46 -0
  26. data/lib/vault/api/transform/role.rb +45 -0
  27. data/lib/vault/api/transform/template.rb +57 -0
  28. data/lib/vault/api/transform/transformation.rb +64 -0
  29. data/lib/vault/api/transform.rb +32 -0
  30. data/lib/vault/api.rb +17 -0
  31. data/lib/vault/client.rb +460 -0
  32. data/lib/vault/configurable.rb +53 -0
  33. data/lib/vault/defaults.rb +218 -0
  34. data/lib/vault/encode.rb +22 -0
  35. data/lib/vault/errors.rb +87 -0
  36. data/lib/vault/persistent/connection.rb +45 -0
  37. data/lib/vault/persistent/pool.rb +51 -0
  38. data/lib/vault/persistent/timed_stack_multi.rb +73 -0
  39. data/lib/vault/persistent.rb +1161 -0
  40. data/lib/vault/request.rb +47 -0
  41. data/lib/vault/response.rb +92 -0
  42. data/lib/vault/vendor/connection_pool/timed_stack.rb +181 -0
  43. data/lib/vault/vendor/connection_pool/version.rb +8 -0
  44. data/lib/vault/vendor/connection_pool.rb +153 -0
  45. data/lib/vault/version.rb +6 -0
  46. data/lib/vault_ruby_client.rb +53 -0
  47. metadata +158 -0
@@ -0,0 +1,460 @@
1
+ # Copyright (c) HashiCorp, Inc.
2
+ # SPDX-License-Identifier: MPL-2.0
3
+
4
+ require "cgi"
5
+ require "json"
6
+ require "uri"
7
+
8
+ require_relative "persistent"
9
+ require_relative "configurable"
10
+ require_relative "errors"
11
+ require_relative "version"
12
+ require_relative "encode"
13
+
14
+ module Vault
15
+ class Client
16
+ # The user agent for this client.
17
+ USER_AGENT = "VaultRuby/#{Vault::VERSION} (+github.com/khiav223577/vault_ruby_client)".freeze
18
+
19
+ # The name of the header used to hold the Vault token.
20
+ TOKEN_HEADER = "X-Vault-Token".freeze
21
+
22
+ # The name of the header used to hold the Namespace.
23
+ NAMESPACE_HEADER = "X-Vault-Namespace".freeze
24
+
25
+ # The name of the header used to hold the wrapped request ttl.
26
+ WRAP_TTL_HEADER = "X-Vault-Wrap-TTL".freeze
27
+
28
+ # The name of the header used for redirection.
29
+ LOCATION_HEADER = "location".freeze
30
+
31
+ # The default headers that are sent with every request.
32
+ DEFAULT_HEADERS = {
33
+ "Content-Type" => "application/json",
34
+ "Accept" => "application/json",
35
+ "User-Agent" => USER_AGENT,
36
+ }.freeze
37
+
38
+ # The default list of options to use when parsing JSON.
39
+ JSON_PARSE_OPTIONS = {
40
+ max_nesting: false,
41
+ create_additions: false,
42
+ symbolize_names: true,
43
+ }.freeze
44
+
45
+ RESCUED_EXCEPTIONS = [].tap do |a|
46
+ # Failure to even open the socket (usually permissions)
47
+ a << SocketError
48
+
49
+ # Failed to reach the server (aka bad URL)
50
+ a << Errno::ECONNREFUSED
51
+ a << Errno::EADDRNOTAVAIL
52
+
53
+ # Failed to read body or no response body given
54
+ a << EOFError
55
+
56
+ # Timeout (Ruby 1.9-)
57
+ a << Timeout::Error
58
+
59
+ # Timeout (Ruby 1.9+) - Ruby 1.9 does not define these constants so we
60
+ # only add them if they are defiend
61
+ a << Net::ReadTimeout if defined?(Net::ReadTimeout)
62
+ a << Net::OpenTimeout if defined?(Net::OpenTimeout)
63
+
64
+ a << PersistentHTTP::Error
65
+ end.freeze
66
+
67
+ # Vault requires at least TLS1.2
68
+ MIN_TLS_VERSION = if defined? OpenSSL::SSL::TLS1_2_VERSION
69
+ OpenSSL::SSL::TLS1_2_VERSION
70
+ else
71
+ "TLSv1_2"
72
+ end
73
+
74
+ include Vault::Configurable
75
+
76
+ # Create a new Client with the given options. Any options given take
77
+ # precedence over the default options.
78
+ #
79
+ # @return [Vault::Client]
80
+ def initialize(options = {})
81
+ # Use any options given, but fall back to the defaults set on the module
82
+ Vault::Configurable.keys.each do |key|
83
+ value = options.key?(key) ? options[key] : Defaults.public_send(key)
84
+ instance_variable_set(:"@#{key}", value)
85
+ end
86
+
87
+ @lock = Mutex.new
88
+ @nhp = nil
89
+ end
90
+
91
+ def pool
92
+ @lock.synchronize do
93
+ return @nhp if @nhp
94
+
95
+ @nhp = PersistentHTTP.new("vault-ruby", nil, pool_size, pool_timeout)
96
+
97
+ if proxy_address
98
+ proxy_uri = URI.parse "http://#{proxy_address}"
99
+
100
+ proxy_uri.port = proxy_port if proxy_port
101
+
102
+ if proxy_username
103
+ proxy_uri.user = proxy_username
104
+ proxy_uri.password = proxy_password
105
+ end
106
+
107
+ @nhp.proxy = proxy_uri
108
+ end
109
+
110
+ # Use a custom open timeout
111
+ if open_timeout || timeout
112
+ @nhp.open_timeout = (open_timeout || timeout).to_i
113
+ end
114
+
115
+ # Use a custom read timeout
116
+ if read_timeout || timeout
117
+ @nhp.read_timeout = (read_timeout || timeout).to_i
118
+ end
119
+
120
+ @nhp.verify_mode = OpenSSL::SSL::VERIFY_PEER
121
+
122
+ @nhp.min_version = MIN_TLS_VERSION
123
+
124
+ # Only use secure ciphers
125
+ @nhp.ciphers = ssl_ciphers
126
+
127
+ # Custom pem files, no problem!
128
+ pem = ssl_pem_contents || (ssl_pem_file ? File.read(ssl_pem_file) : nil)
129
+ if pem
130
+ @nhp.cert = OpenSSL::X509::Certificate.new(pem)
131
+ @nhp.key = OpenSSL::PKey::RSA.new(pem, ssl_pem_passphrase)
132
+ end
133
+
134
+ # Use custom CA cert for verification
135
+ if ssl_ca_cert
136
+ @nhp.ca_file = ssl_ca_cert
137
+ end
138
+
139
+ # Use custom CA path that contains CA certs
140
+ if ssl_ca_path
141
+ @nhp.ca_path = ssl_ca_path
142
+ end
143
+
144
+ if ssl_cert_store
145
+ @nhp.cert_store = ssl_cert_store
146
+ end
147
+
148
+ # Naughty, naughty, naughty! Don't blame me when someone hops in
149
+ # and executes a MITM attack!
150
+ if !ssl_verify
151
+ @nhp.verify_mode = OpenSSL::SSL::VERIFY_NONE
152
+ end
153
+
154
+ # Use custom timeout for connecting and verifying via SSL
155
+ if ssl_timeout || timeout
156
+ @nhp.ssl_timeout = (ssl_timeout || timeout).to_i
157
+ end
158
+
159
+ @nhp
160
+ end
161
+ end
162
+
163
+ private :pool
164
+
165
+ # Shutdown any open pool connections. Pool will be recreated upon next request.
166
+ def shutdown
167
+ @nhp.shutdown()
168
+ @nhp = nil
169
+ end
170
+
171
+ # Creates and yields a new client object with the given token. This may be
172
+ # used safely in a threadsafe manner because the original client remains
173
+ # unchanged. The value of the block is returned.
174
+ #
175
+ # @yield [Vault::Client]
176
+ def with_token(token)
177
+ client = self.dup
178
+ client.token = token
179
+ return yield client if block_given?
180
+ return nil
181
+ end
182
+
183
+ # Determine if the given options are the same as ours.
184
+ # @return [true, false]
185
+ def same_options?(opts)
186
+ options.hash == opts.hash
187
+ end
188
+
189
+ # Perform a GET request.
190
+ # @see Client#request
191
+ def get(path, params = {}, headers = {})
192
+ request(:get, path, params, headers)
193
+ end
194
+
195
+ # Perform a LIST request.
196
+ # @see Client#request
197
+ def list(path, params = {}, headers = {})
198
+ params = params.merge(list: true)
199
+ request(:get, path, params, headers)
200
+ end
201
+
202
+ # Perform a POST request.
203
+ # @see Client#request
204
+ def post(path, data = {}, headers = {})
205
+ request(:post, path, data, headers)
206
+ end
207
+
208
+ # Perform a PUT request.
209
+ # @see Client#request
210
+ def put(path, data, headers = {})
211
+ request(:put, path, data, headers)
212
+ end
213
+
214
+ # Perform a PATCH request.
215
+ # @see Client#request
216
+ def patch(path, data, headers = {})
217
+ request(:patch, path, data, headers)
218
+ end
219
+
220
+ # Perform a DELETE request.
221
+ # @see Client#request
222
+ def delete(path, params = {}, headers = {})
223
+ request(:delete, path, params, headers)
224
+ end
225
+
226
+ # Make an HTTP request with the given verb, data, params, and headers. If
227
+ # the response has a return type of JSON, the JSON is automatically parsed
228
+ # and returned as a hash; otherwise it is returned as a string.
229
+ #
230
+ # @raise [HTTPError]
231
+ # if the request is not an HTTP 200 OK
232
+ #
233
+ # @param [Symbol] verb
234
+ # the lowercase symbol of the HTTP verb (e.g. :get, :delete)
235
+ # @param [String] path
236
+ # the absolute or relative path from {Defaults.address} to make the
237
+ # request against
238
+ # @param [#read, Hash, nil] data
239
+ # the data to use (varies based on the +verb+)
240
+ # @param [Hash] headers
241
+ # the list of headers to use
242
+ #
243
+ # @return [String, Hash]
244
+ # the response body
245
+ def request(verb, path, data = {}, headers = {})
246
+ # Build the URI and request object from the given information
247
+ uri = build_uri(verb, path, data)
248
+ request = class_for_request(verb).new(uri.request_uri)
249
+ if uri.userinfo()
250
+ request.basic_auth uri.user, uri.password
251
+ end
252
+
253
+ # Get a list of headers
254
+ headers = DEFAULT_HEADERS.merge(headers)
255
+
256
+ # Add the Vault token header - users could still override this on a
257
+ # per-request basis
258
+ if !token.nil?
259
+ headers[TOKEN_HEADER] ||= token
260
+ end
261
+
262
+ # Add the Vault Namespace header - users could still override this on a
263
+ # per-request basis
264
+ if !namespace.nil?
265
+ headers[NAMESPACE_HEADER] ||= namespace
266
+ end
267
+
268
+ # Add headers
269
+ headers.each do |key, value|
270
+ request.add_field(key, value)
271
+ end
272
+
273
+ # Setup PATCH/POST/PUT
274
+ if [:patch, :post, :put].include?(verb)
275
+ if data.respond_to?(:read)
276
+ request.content_length = data.size
277
+ request.body_stream = data
278
+ elsif data.is_a?(Hash)
279
+ request.form_data = data
280
+ else
281
+ request.body = data
282
+ end
283
+ end
284
+
285
+ begin
286
+ # Create a connection using the block form, which will ensure the socket
287
+ # is properly closed in the event of an error.
288
+ response = pool.request(uri, request)
289
+
290
+ case response
291
+ when Net::HTTPRedirection
292
+ # On a redirect of a GET or HEAD request, the URL already contains
293
+ # the data as query string parameters.
294
+ if [:head, :get].include?(verb)
295
+ data = {}
296
+ end
297
+ request(verb, response[LOCATION_HEADER], data, headers)
298
+ when Net::HTTPSuccess
299
+ success(response)
300
+ else
301
+ error(response)
302
+ end
303
+ rescue *RESCUED_EXCEPTIONS => e
304
+ raise HTTPConnectionError.new(address, e)
305
+ end
306
+ end
307
+
308
+ # Construct a URL from the given verb and path. If the request is a GET or
309
+ # DELETE request, the params are assumed to be query params are are
310
+ # converted as such using {Client#to_query_string}.
311
+ #
312
+ # If the path is relative, it is merged with the {Defaults.address}
313
+ # attribute. If the path is absolute, it is converted to a URI object and
314
+ # returned.
315
+ #
316
+ # @param [Symbol] verb
317
+ # the lowercase HTTP verb (e.g. :+get+)
318
+ # @param [String] path
319
+ # the absolute or relative HTTP path (url) to get
320
+ # @param [Hash] params
321
+ # the list of params to build the URI with (for GET and DELETE requests)
322
+ #
323
+ # @return [URI]
324
+ def build_uri(verb, path, params = {})
325
+ # Add any query string parameters
326
+ if [:delete, :get].include?(verb)
327
+ path = [path, to_query_string(params)].compact.join("?")
328
+ end
329
+
330
+ # Parse the URI
331
+ uri = URI.parse(path)
332
+
333
+ # Don't merge absolute URLs
334
+ uri = URI.parse(File.join(address, path)) unless uri.absolute?
335
+
336
+ # Return the URI object
337
+ uri
338
+ end
339
+
340
+ # Helper method to get the corresponding {Net::HTTP} class from the given
341
+ # HTTP verb.
342
+ #
343
+ # @param [#to_s] verb
344
+ # the HTTP verb to create a class from
345
+ #
346
+ # @return [Class]
347
+ def class_for_request(verb)
348
+ Net::HTTP.const_get(verb.to_s.capitalize)
349
+ end
350
+
351
+ # Convert the given hash to a list of query string parameters. Each key and
352
+ # value in the hash is URI-escaped for safety.
353
+ #
354
+ # @param [Hash] hash
355
+ # the hash to create the query string from
356
+ #
357
+ # @return [String, nil]
358
+ # the query string as a string, or +nil+ if there are no params
359
+ def to_query_string(hash)
360
+ hash.map do |key, value|
361
+ "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"
362
+ end.join('&')[/.+/]
363
+ end
364
+
365
+ # Parse the response object and manipulate the result based on the given
366
+ # +Content-Type+ header. For now, this method only parses JSON, but it
367
+ # could be expanded in the future to accept other content types.
368
+ #
369
+ # @param [HTTP::Message] response
370
+ # the response object from the request
371
+ #
372
+ # @return [String, Hash]
373
+ # the parsed response, as an object
374
+ def success(response)
375
+ if response.body && (response.content_type || '').include?("json")
376
+ JSON.parse(response.body, JSON_PARSE_OPTIONS)
377
+ else
378
+ response.body
379
+ end
380
+ end
381
+
382
+ # Raise a response error, extracting as much information from the server's
383
+ # response as possible.
384
+ #
385
+ # @raise [HTTPError]
386
+ #
387
+ # @param [HTTP::Message] response
388
+ # the response object from the request
389
+ def error(response)
390
+ if response.body && response.body.match("missing client token")
391
+ # Vault 1.10+ no longer returns "missing" client token" so we use HTTPClientError
392
+ klass = HTTPClientError
393
+ else
394
+ # Use the correct exception class
395
+ case response
396
+ when Net::HTTPPreconditionFailed
397
+ raise MissingRequiredStateError.new
398
+ when Net::HTTPClientError
399
+ klass = HTTPClientError
400
+ when Net::HTTPServerError
401
+ klass = HTTPServerError
402
+ else
403
+ klass = HTTPError
404
+ end
405
+ end
406
+
407
+ if (response.content_type || '').include?("json")
408
+ # Attempt to parse the error as JSON
409
+ begin
410
+ json = JSON.parse(response.body, JSON_PARSE_OPTIONS)
411
+
412
+ if json[:errors]
413
+ raise klass.new(address, response, json[:errors])
414
+ end
415
+ rescue JSON::ParserError; end
416
+ end
417
+
418
+ raise klass.new(address, response, [response.body])
419
+ end
420
+
421
+ # Execute the given block with retries and exponential backoff.
422
+ #
423
+ # @param [Array<Exception>] rescued
424
+ # the list of exceptions to rescue
425
+ def with_retries(*rescued, &block)
426
+ options = rescued.last.is_a?(Hash) ? rescued.pop : {}
427
+ exception = nil
428
+ retries = 0
429
+
430
+ rescued = Defaults::RETRIED_EXCEPTIONS if rescued.empty?
431
+
432
+ max_attempts = options[:attempts] || Defaults::RETRY_ATTEMPTS
433
+ backoff_base = options[:base] || Defaults::RETRY_BASE
434
+ backoff_max = options[:max_wait] || Defaults::RETRY_MAX_WAIT
435
+
436
+ begin
437
+ return yield retries, exception
438
+ rescue *rescued => e
439
+ exception = e
440
+
441
+ retries += 1
442
+ raise if retries > max_attempts
443
+
444
+ # Calculate the exponential backoff combined with an element of
445
+ # randomness.
446
+ backoff = [backoff_base * (2 ** (retries - 1)), backoff_max].min
447
+ backoff = backoff * (0.5 * (1 + Kernel.rand))
448
+
449
+ # Ensure we are sleeping at least the minimum interval.
450
+ backoff = [backoff_base, backoff].max
451
+
452
+ # Exponential backoff.
453
+ Kernel.sleep(backoff)
454
+
455
+ # Now retry
456
+ retry
457
+ end
458
+ end
459
+ end
460
+ end
@@ -0,0 +1,53 @@
1
+ # Copyright (c) HashiCorp, Inc.
2
+ # SPDX-License-Identifier: MPL-2.0
3
+
4
+ require_relative "defaults"
5
+
6
+ module Vault
7
+ module Configurable
8
+ def self.keys
9
+ @keys ||= [
10
+ :address,
11
+ :token,
12
+ :hostname,
13
+ :namespace,
14
+ :open_timeout,
15
+ :proxy_address,
16
+ :proxy_password,
17
+ :proxy_port,
18
+ :proxy_username,
19
+ :pool_size,
20
+ :pool_timeout,
21
+ :read_timeout,
22
+ :ssl_ciphers,
23
+ :ssl_pem_contents,
24
+ :ssl_pem_file,
25
+ :ssl_pem_passphrase,
26
+ :ssl_ca_cert,
27
+ :ssl_ca_path,
28
+ :ssl_cert_store,
29
+ :ssl_verify,
30
+ :ssl_timeout,
31
+ :timeout,
32
+ ]
33
+ end
34
+
35
+ Vault::Configurable.keys.each(&method(:attr_accessor))
36
+
37
+ # Configure yields self for block-style configuration.
38
+ #
39
+ # @yield [self]
40
+ def configure
41
+ yield self
42
+ end
43
+
44
+ # The list of options for this configurable.
45
+ #
46
+ # @return [Hash<Symbol, Object>]
47
+ def options
48
+ Hash[*Vault::Configurable.keys.map do |key|
49
+ [key, instance_variable_get(:"@#{key}")]
50
+ end.flatten]
51
+ end
52
+ end
53
+ end