vault 0.6.0 → 0.7.0

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.
@@ -39,5 +39,10 @@ module Vault
39
39
  json = client.get("/v1/sys/leader")
40
40
  return LeaderStatus.decode(json)
41
41
  end
42
+
43
+ def step_down
44
+ client.put("/v1/sys/step-down", nil)
45
+ return true
46
+ end
42
47
  end
43
48
  end
@@ -48,7 +48,7 @@ module Vault
48
48
  payload = { type: type }
49
49
  payload[:description] = description if !description.nil?
50
50
 
51
- client.post("/v1/sys/mounts/#{CGI.escape(path)}", JSON.fast_generate(payload))
51
+ client.post("/v1/sys/mounts/#{encode_path(path)}", JSON.fast_generate(payload))
52
52
  return true
53
53
  end
54
54
 
@@ -62,7 +62,7 @@ module Vault
62
62
  # @param [Hash] data
63
63
  # the data to write
64
64
  def mount_tune(path, data = {})
65
- json = client.post("/v1/sys/mounts/#{CGI.escape(path)}/tune", JSON.fast_generate(data))
65
+ json = client.post("/v1/sys/mounts/#{encode_path(path)}/tune", JSON.fast_generate(data))
66
66
  return true
67
67
  end
68
68
 
@@ -77,7 +77,7 @@ module Vault
77
77
  #
78
78
  # @return [true]
79
79
  def unmount(path)
80
- client.delete("/v1/sys/mounts/#{CGI.escape(path)}")
80
+ client.delete("/v1/sys/mounts/#{encode_path(path)}")
81
81
  return true
82
82
  end
83
83
 
@@ -40,7 +40,7 @@ module Vault
40
40
  #
41
41
  # @return [Policy, nil]
42
42
  def policy(name)
43
- json = client.get("/v1/sys/policy/#{CGI.escape(name)}")
43
+ json = client.get("/v1/sys/policy/#{encode_path(name)}")
44
44
  return Policy.decode(json)
45
45
  rescue HTTPError => e
46
46
  return nil if e.code == 404
@@ -70,7 +70,7 @@ module Vault
70
70
  #
71
71
  # @return [true]
72
72
  def put_policy(name, rules)
73
- client.put("/v1/sys/policy/#{CGI.escape(name)}", JSON.fast_generate(
73
+ client.put("/v1/sys/policy/#{encode_path(name)}", JSON.fast_generate(
74
74
  rules: rules,
75
75
  ))
76
76
  return true
@@ -85,7 +85,7 @@ module Vault
85
85
  # @param [String] name
86
86
  # the name of the policy
87
87
  def delete_policy(name)
88
- client.delete("/v1/sys/policy/#{CGI.escape(name)}")
88
+ client.delete("/v1/sys/policy/#{encode_path(name)}")
89
89
  return true
90
90
  end
91
91
  end
@@ -1,12 +1,13 @@
1
1
  require "cgi"
2
2
  require "json"
3
- require "net/http"
4
- require "net/https"
5
3
  require "uri"
6
4
 
5
+ require_relative "vendor/net/http/persistent"
6
+
7
7
  require_relative "configurable"
8
8
  require_relative "errors"
9
9
  require_relative "version"
10
+ require_relative "encode"
10
11
 
11
12
  module Vault
12
13
  class Client
@@ -53,8 +54,15 @@ module Vault
53
54
  # only add them if they are defiend
54
55
  a << Net::ReadTimeout if defined?(Net::ReadTimeout)
55
56
  a << Net::OpenTimeout if defined?(Net::OpenTimeout)
57
+
58
+ a << Net::HTTP::Persistent::Error
56
59
  end.freeze
57
60
 
61
+ # Indicates a requested operation is not possible due to security
62
+ # concerns.
63
+ class SecurityError < RuntimeError
64
+ end
65
+
58
66
  include Vault::Configurable
59
67
 
60
68
  # Create a new Client with the given options. Any options given take
@@ -67,6 +75,71 @@ module Vault
67
75
  value = options.key?(key) ? options[key] : Defaults.public_send(key)
68
76
  instance_variable_set(:"@#{key}", value)
69
77
  end
78
+
79
+ @nhp = Net::HTTP::Persistent.new(name: "vault-ruby")
80
+
81
+ if proxy_address
82
+ proxy_uri = URI.parse "http://#{proxy_address}"
83
+
84
+ proxy_uri.port = proxy_port if proxy_port
85
+
86
+ if proxy_username
87
+ proxy_uri.user = proxy_username
88
+ proxy_uri.password = proxy_password
89
+ end
90
+
91
+ @nhp.proxy = proxy_uri
92
+ end
93
+
94
+ # Use a custom open timeout
95
+ if open_timeout || timeout
96
+ @nhp.open_timeout = (open_timeout || timeout).to_i
97
+ end
98
+
99
+ # Use a custom read timeout
100
+ if read_timeout || timeout
101
+ @nhp.read_timeout = (read_timeout || timeout).to_i
102
+ end
103
+
104
+ @nhp.verify_mode = OpenSSL::SSL::VERIFY_PEER
105
+
106
+ # Vault requires TLS1.2
107
+ @nhp.ssl_version = "TLSv1_2"
108
+
109
+ # Only use secure ciphers
110
+ @nhp.ciphers = ssl_ciphers
111
+
112
+ # Custom pem files, no problem!
113
+ pem = ssl_pem_contents || (ssl_pem_file ? File.read(ssl_pem_file) : nil)
114
+ if pem
115
+ @nhp.cert = OpenSSL::X509::Certificate.new(pem)
116
+ @nhp.key = OpenSSL::PKey::RSA.new(pem, ssl_pem_passphrase)
117
+ end
118
+
119
+ # Use custom CA cert for verification
120
+ if ssl_ca_cert
121
+ @nhp.ca_file = ssl_ca_cert
122
+ end
123
+
124
+ # Use custom CA path that contains CA certs
125
+ if ssl_ca_path
126
+ @nhp.ca_path = ssl_ca_path
127
+ end
128
+
129
+ if ssl_cert_store
130
+ @nhp.cert_store = ssl_cert_store
131
+ end
132
+
133
+ # Naughty, naughty, naughty! Don't blame me when someone hops in
134
+ # and executes a MITM attack!
135
+ if !ssl_verify
136
+ @nhp.verify_mode = OpenSSL::SSL::VERIFY_NONE
137
+ end
138
+
139
+ # Use custom timeout for connecting and verifying via SSL
140
+ if ssl_timeout || timeout
141
+ @nhp.ssl_timeout = (ssl_timeout || timeout).to_i
142
+ end
70
143
  end
71
144
 
72
145
  # Creates and yields a new client object with the given token. This may be
@@ -148,6 +221,10 @@ module Vault
148
221
  uri = build_uri(verb, path, data)
149
222
  request = class_for_request(verb).new(uri.request_uri)
150
223
 
224
+ if proxy_address and uri.scheme.downcase == "https"
225
+ raise SecurityError, "no direct https connection to vault"
226
+ end
227
+
151
228
  # Get a list of headers
152
229
  headers = DEFAULT_HEADERS.merge(headers)
153
230
 
@@ -174,82 +251,23 @@ module Vault
174
251
  end
175
252
  end
176
253
 
177
- # Create the HTTP connection object - since the proxy information defaults
178
- # to +nil+, we can just pass it to the initializer method instead of doing
179
- # crazy strange conditionals.
180
- connection = Net::HTTP.new(uri.host, uri.port,
181
- proxy_address, proxy_port, proxy_username, proxy_password)
182
-
183
- # Use a custom open timeout
184
- if open_timeout || timeout
185
- connection.open_timeout = (open_timeout || timeout).to_i
186
- end
187
-
188
- # Use a custom read timeout
189
- if read_timeout || timeout
190
- connection.read_timeout = (read_timeout || timeout).to_i
191
- end
192
-
193
- # Apply SSL, if applicable
194
- if uri.scheme == "https"
195
- # Turn on SSL
196
- connection.use_ssl = true
197
-
198
- # Vault requires TLS1.2
199
- connection.ssl_version = "TLSv1_2"
200
-
201
- # Only use secure ciphers
202
- connection.ciphers = ssl_ciphers
203
-
204
- # Custom pem files, no problem!
205
- pem = ssl_pem_contents || ssl_pem_file ? File.read(ssl_pem_file) : nil
206
- if pem
207
- connection.cert = OpenSSL::X509::Certificate.new(pem)
208
- connection.key = OpenSSL::PKey::RSA.new(pem, ssl_pem_passphrase)
209
- connection.verify_mode = OpenSSL::SSL::VERIFY_PEER
210
- end
211
-
212
- # Use custom CA cert for verification
213
- if ssl_ca_cert
214
- connection.ca_file = ssl_ca_cert
215
- end
216
-
217
- # Use custom CA path that contains CA certs
218
- if ssl_ca_path
219
- connection.ca_path = ssl_ca_path
220
- end
221
-
222
- # Naughty, naughty, naughty! Don't blame me when someone hops in
223
- # and executes a MITM attack!
224
- if !ssl_verify
225
- connection.verify_mode = OpenSSL::SSL::VERIFY_NONE
226
- end
227
-
228
- # Use custom timeout for connecting and verifying via SSL
229
- if ssl_timeout || timeout
230
- connection.ssl_timeout = (ssl_timeout || timeout).to_i
231
- end
232
- end
233
-
234
254
  begin
235
255
  # Create a connection using the block form, which will ensure the socket
236
256
  # is properly closed in the event of an error.
237
- connection.start do |http|
238
- response = http.request(request)
239
-
240
- case response
241
- when Net::HTTPRedirection
242
- # On a redirect of a GET or HEAD request, the URL already contains
243
- # the data as query string parameters.
244
- if [:head, :get].include?(verb)
245
- data = {}
246
- end
247
- request(verb, response[LOCATION_HEADER], data, headers)
248
- when Net::HTTPSuccess
249
- success(response)
250
- else
251
- error(response)
257
+ response = @nhp.request(uri, request)
258
+
259
+ case response
260
+ when Net::HTTPRedirection
261
+ # On a redirect of a GET or HEAD request, the URL already contains
262
+ # the data as query string parameters.
263
+ if [:head, :get].include?(verb)
264
+ data = {}
252
265
  end
266
+ request(verb, response[LOCATION_HEADER], data, headers)
267
+ when Net::HTTPSuccess
268
+ success(response)
269
+ else
270
+ error(response)
253
271
  end
254
272
  rescue *RESCUED_EXCEPTIONS => e
255
273
  raise HTTPConnectionError.new(address, e)
@@ -18,6 +18,7 @@ module Vault
18
18
  :ssl_pem_passphrase,
19
19
  :ssl_ca_cert,
20
20
  :ssl_ca_path,
21
+ :ssl_cert_store,
21
22
  :ssl_verify,
22
23
  :ssl_timeout,
23
24
  :timeout,
@@ -123,7 +123,13 @@ module Vault
123
123
  def ssl_ca_cert
124
124
  ENV["VAULT_CACERT"]
125
125
  end
126
- #
126
+
127
+ # The CA cert store to use for certificate verification
128
+ # @return [OpenSSL::X509::Store, nil]
129
+ def ssl_cert_store
130
+ nil
131
+ end
132
+
127
133
  # The path to the directory on disk holding CA certs to use
128
134
  # for certificate verification
129
135
  # @return [String, nil]
@@ -0,0 +1,19 @@
1
+ module Vault
2
+ module EncodePath
3
+
4
+ # Encodes a string according to the rules for URL paths. This is
5
+ # used as opposed to CGI.escape because in a URL path, space
6
+ # needs to be escaped as %20 and CGI.escapes a space as +.
7
+ #
8
+ # @param [String]
9
+ #
10
+ # @return [String]
11
+ def encode_path(path)
12
+ path.b.gsub(%r!([^a-zA-Z0-9_.-/]+)!) { |m|
13
+ '%' + m.unpack('H2' * m.bytesize).join('%').upcase
14
+ }
15
+ end
16
+
17
+ module_function :encode_path
18
+ end
19
+ end
@@ -18,6 +18,8 @@ module Vault
18
18
 
19
19
  private
20
20
 
21
+ include EncodePath
22
+
21
23
  # Removes the given header fields from options and returns the result. This
22
24
  # modifies the given options in place.
23
25
  #
@@ -0,0 +1,150 @@
1
+ require_relative 'connection_pool/version'
2
+ require_relative 'connection_pool/timed_stack'
3
+
4
+
5
+ # Generic connection pool class for e.g. sharing a limited number of network connections
6
+ # among many threads. Note: Connections are lazily created.
7
+ #
8
+ # Example usage with block (faster):
9
+ #
10
+ # @pool = ConnectionPool.new { Redis.new }
11
+ #
12
+ # @pool.with do |redis|
13
+ # redis.lpop('my-list') if redis.llen('my-list') > 0
14
+ # end
15
+ #
16
+ # Using optional timeout override (for that single invocation)
17
+ #
18
+ # @pool.with(:timeout => 2.0) do |redis|
19
+ # redis.lpop('my-list') if redis.llen('my-list') > 0
20
+ # end
21
+ #
22
+ # Example usage replacing an existing connection (slower):
23
+ #
24
+ # $redis = ConnectionPool.wrap { Redis.new }
25
+ #
26
+ # def do_work
27
+ # $redis.lpop('my-list') if $redis.llen('my-list') > 0
28
+ # end
29
+ #
30
+ # Accepts the following options:
31
+ # - :size - number of connections to pool, defaults to 5
32
+ # - :timeout - amount of time to wait for a connection if none currently available, defaults to 5 seconds
33
+ #
34
+ module Vault
35
+ class ConnectionPool
36
+ DEFAULTS = {size: 5, timeout: 5}
37
+
38
+ class Error < RuntimeError
39
+ end
40
+
41
+ def self.wrap(options, &block)
42
+ Wrapper.new(options, &block)
43
+ end
44
+
45
+ def initialize(options = {}, &block)
46
+ raise ArgumentError, 'Connection pool requires a block' unless block
47
+
48
+ options = DEFAULTS.merge(options)
49
+
50
+ @size = options.fetch(:size)
51
+ @timeout = options.fetch(:timeout)
52
+
53
+ @available = TimedStack.new(@size, &block)
54
+ @key = :"current-#{@available.object_id}"
55
+ end
56
+
57
+ if Thread.respond_to?(:handle_interrupt)
58
+
59
+ # MRI
60
+ def with(options = {})
61
+ Thread.handle_interrupt(Exception => :never) do
62
+ conn = checkout(options)
63
+ begin
64
+ Thread.handle_interrupt(Exception => :immediate) do
65
+ yield conn
66
+ end
67
+ ensure
68
+ checkin
69
+ end
70
+ end
71
+ end
72
+
73
+ else
74
+
75
+ # jruby 1.7.x
76
+ def with(options = {})
77
+ conn = checkout(options)
78
+ begin
79
+ yield conn
80
+ ensure
81
+ checkin
82
+ end
83
+ end
84
+
85
+ end
86
+
87
+ def checkout(options = {})
88
+ conn = if stack.empty?
89
+ timeout = options[:timeout] || @timeout
90
+ @available.pop(timeout: timeout)
91
+ else
92
+ stack.last
93
+ end
94
+
95
+ stack.push conn
96
+ conn
97
+ end
98
+
99
+ def checkin
100
+ conn = pop_connection # mutates stack, must be on its own line
101
+ @available.push(conn) if stack.empty?
102
+
103
+ nil
104
+ end
105
+
106
+ def shutdown(&block)
107
+ @available.shutdown(&block)
108
+ end
109
+
110
+ private
111
+
112
+ def pop_connection
113
+ if stack.empty?
114
+ raise ConnectionPool::Error, 'no connections are checked out'
115
+ else
116
+ stack.pop
117
+ end
118
+ end
119
+
120
+ def stack
121
+ ::Thread.current[@key] ||= []
122
+ end
123
+
124
+ class Wrapper < ::BasicObject
125
+ METHODS = [:with, :pool_shutdown]
126
+
127
+ def initialize(options = {}, &block)
128
+ @pool = ::ConnectionPool.new(options, &block)
129
+ end
130
+
131
+ def with(&block)
132
+ @pool.with(&block)
133
+ end
134
+
135
+ def pool_shutdown(&block)
136
+ @pool.shutdown(&block)
137
+ end
138
+
139
+ def respond_to?(id, *args)
140
+ METHODS.include?(id) || with { |c| c.respond_to?(id, *args) }
141
+ end
142
+
143
+ def method_missing(name, *args, &block)
144
+ with do |connection|
145
+ connection.send(name, *args, &block)
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end