vault 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +41 -0
- data/.rspec +2 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +32 -0
- data/LICENSE +362 -0
- data/README.md +80 -54
- data/Rakefile +4 -40
- data/lib/vault.rb +33 -46
- data/lib/vault/api.rb +9 -0
- data/lib/vault/api/auth_token.rb +90 -0
- data/lib/vault/api/help.rb +23 -0
- data/lib/vault/api/logical.rb +66 -0
- data/lib/vault/api/secret.rb +23 -0
- data/lib/vault/api/sys.rb +24 -0
- data/lib/vault/api/sys/audit.rb +60 -0
- data/lib/vault/api/sys/auth.rb +58 -0
- data/lib/vault/api/sys/init.rb +46 -0
- data/lib/vault/api/sys/leader.rb +25 -0
- data/lib/vault/api/sys/lease.rb +51 -0
- data/lib/vault/api/sys/mount.rb +75 -0
- data/lib/vault/api/sys/policy.rb +76 -0
- data/lib/vault/api/sys/seal.rb +49 -0
- data/lib/vault/client.rb +285 -0
- data/lib/vault/configurable.rb +48 -0
- data/lib/vault/defaults.rb +68 -0
- data/lib/vault/errors.rb +48 -0
- data/lib/vault/request.rb +19 -0
- data/lib/vault/response.rb +20 -0
- data/lib/vault/version.rb +1 -6
- data/vault.gemspec +25 -0
- metadata +97 -98
- data/MIT-LICENSE +0 -20
- data/lib/vault/associations.rb +0 -39
- data/lib/vault/attribute_accessors.rb +0 -29
- data/lib/vault/bulk_attributes.rb +0 -17
- data/lib/vault/dirty.rb +0 -37
- data/lib/vault/finders.rb +0 -24
- data/lib/vault/persistance.rb +0 -47
- data/lib/vault/properties.rb +0 -68
- data/lib/vault/scoping.rb +0 -64
- data/lib/vault/storage.rb +0 -4
- data/lib/vault/storage/in_memory_store.rb +0 -14
- data/lib/vault/storage/yaml_store.rb +0 -52
- data/lib/vault/validations.rb +0 -13
- data/spec/active_model_compliance_spec.rb +0 -33
- data/spec/spec_helper.rb +0 -8
- data/spec/support/helpers.rb +0 -16
- data/spec/support/storage_api.rb +0 -14
- data/spec/vault/associations_spec.rb +0 -73
- data/spec/vault/finders_spec.rb +0 -69
- data/spec/vault/persistance_spec.rb +0 -126
- data/spec/vault/properties_spec.rb +0 -59
- data/spec/vault/scoping_spec.rb +0 -53
- data/spec/vault/storage/in_memory_store_spec.rb +0 -5
- data/spec/vault/storage/yaml_store_spec.rb +0 -29
- 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
|
data/lib/vault/client.rb
ADDED
@@ -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
|