vault 0.1.0 → 0.1.1
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.
- 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
|