vault_ruby_client 0.18.2

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 (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