vault 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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