cerberus_client 1.2.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 +15 -0
- data/.gitignore +13 -0
- data/.rspec +2 -0
- data/CHANGELOG.md +8 -0
- data/CONTRIBUTING.md +39 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +202 -0
- data/README.md +71 -0
- data/cerberus_client.gemspec +33 -0
- data/lib/cerberus/assumed_role_credentials_provider_chain.rb +45 -0
- data/lib/cerberus/aws_role_credentials_provider.rb +200 -0
- data/lib/cerberus/aws_role_info.rb +15 -0
- data/lib/cerberus/cerberus_client_token.rb +37 -0
- data/lib/cerberus/default_credentials_provider_chain.rb +44 -0
- data/lib/cerberus/default_url_resolver.rb +26 -0
- data/lib/cerberus/env_credentials_provider.rb +29 -0
- data/lib/cerberus/exception/ambiguous_vault_bad_request.rb +22 -0
- data/lib/cerberus/exception/http_error.rb +24 -0
- data/lib/cerberus/exception/no_valid_providers.rb +17 -0
- data/lib/cerberus/exception/no_value_error.rb +19 -0
- data/lib/cerberus/vault_client.rb +207 -0
- data/lib/cerberus_client.rb +62 -0
- data/lib/cerberus_client/default_logger.rb +64 -0
- data/lib/cerberus_client/http.rb +67 -0
- data/lib/cerberus_client/log.rb +59 -0
- data/lib/cerberus_client/version.rb +3 -0
- metadata +102 -0
@@ -0,0 +1,207 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
module Cerberus
|
4
|
+
|
5
|
+
require_relative('../cerberus_client/log')
|
6
|
+
require_relative('exception/http_error')
|
7
|
+
require_relative('exception/ambiguous_vault_bad_request')
|
8
|
+
require_relative('default_credentials_provider_chain')
|
9
|
+
require('json')
|
10
|
+
|
11
|
+
##
|
12
|
+
# Client for interacting with the Vault API
|
13
|
+
##
|
14
|
+
class VaultClient
|
15
|
+
|
16
|
+
# relative path to the Vault secrets API
|
17
|
+
SECRET_PATH_PREFIX = "/v1/secret/"
|
18
|
+
SECRET_MAP_DATA_KEY = VAULT_LIST_DATA_KEY = "data"
|
19
|
+
VAULT_TOKEN_HEADER_KEY = 'X-Vault-Token'
|
20
|
+
VAULT_ERRORS_KEY = "errors"
|
21
|
+
VAULT_PERMISSION_DENIED_ERR = "permission denied"
|
22
|
+
VAULT_LIST_KEYS_KEY = "keys"
|
23
|
+
VAULT_LIST_PARAM_KEY = "list"
|
24
|
+
SLASH = "/"
|
25
|
+
|
26
|
+
attr_reader :vaultBaseUrl
|
27
|
+
attr_reader :credentialsProvider
|
28
|
+
|
29
|
+
##
|
30
|
+
# Init with the base URL for vault
|
31
|
+
##
|
32
|
+
def initialize(urlResolver, credentialsProviderChain)
|
33
|
+
|
34
|
+
require 'net/https'
|
35
|
+
|
36
|
+
@vaultBaseUrl = CerberusClient.getUrlFromResolver(urlResolver)
|
37
|
+
@credentialsProvider = credentialsProviderChain.getCredentialsProvider
|
38
|
+
|
39
|
+
end # initialize
|
40
|
+
|
41
|
+
##
|
42
|
+
# Read operation for a specified path.
|
43
|
+
##
|
44
|
+
def read(path)
|
45
|
+
|
46
|
+
begin
|
47
|
+
response = doVaultHttpGet(SECRET_PATH_PREFIX + path)
|
48
|
+
CerberusClient::Log.instance.debug("VaultClient::read(path) HTTP response: #{response.code}, #{response.message}")
|
49
|
+
response.body
|
50
|
+
|
51
|
+
rescue => ex
|
52
|
+
CerberusClient::Log.instance.error("VaultClient::read(#{path}) unhandled exception trying to read: #{ex.message}")
|
53
|
+
raise ex
|
54
|
+
end
|
55
|
+
end # read
|
56
|
+
|
57
|
+
##
|
58
|
+
# Read operation for a specified path.
|
59
|
+
##
|
60
|
+
def readKey(path, key)
|
61
|
+
begin
|
62
|
+
CerberusClient::Log.instance.error("VaultClient::read(#{path}, #{key})")
|
63
|
+
|
64
|
+
readPathAndIterateOnDataWithProc(path, &(Proc.new { |k, v| if(key == k); return v; end }) )
|
65
|
+
|
66
|
+
# else, we didn't find it
|
67
|
+
return nil
|
68
|
+
|
69
|
+
rescue => ex
|
70
|
+
CerberusClient::Log.instance.error("VaultClient::read(#{path}, #{key}) unhandled exception trying to read: #{ex.message}")
|
71
|
+
raise ex
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
##
|
76
|
+
# Returns a list of key names at the specified location. Folders are suffixed with /.
|
77
|
+
# The input must be a folder; list on a file will return nil
|
78
|
+
##
|
79
|
+
def list(path)
|
80
|
+
begin
|
81
|
+
response = doVaultHttpGet(SECRET_PATH_PREFIX + path + "?list=true")
|
82
|
+
|
83
|
+
CerberusClient::Log.instance.debug("VaultClient::list(#{path}) HTTP response: #{response.code}, #{response.message}")
|
84
|
+
|
85
|
+
jsonResonseBody = JSON.parse(response.body)
|
86
|
+
pathList = jsonResonseBody[VAULT_LIST_DATA_KEY][VAULT_LIST_KEYS_KEY]
|
87
|
+
CerberusClient::Log.instance.debug("VaultClient::list returning #{pathList.join(", ")} ")
|
88
|
+
pathList
|
89
|
+
|
90
|
+
rescue => ex
|
91
|
+
|
92
|
+
# check to see if we threw the Http error with a response object
|
93
|
+
response = (ex.instance_of?(Cerberus::Exception::HttpError)) ? ex.response : nil
|
94
|
+
if(!response.nil? && response.code.to_i == 404)
|
95
|
+
return nil
|
96
|
+
end
|
97
|
+
|
98
|
+
CerberusClient::Log.instance.error("VaultClient::list(#{path}) unhandled exception trying to read: #{ex.message}")
|
99
|
+
raise ex
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
##
|
104
|
+
# This is potentially an expensive operation depending on the depth of the tree we're trying to parse
|
105
|
+
# It recursively walks 'path' and returns a hash of all child [path] => [array of keys] found under 'path'
|
106
|
+
# if 'path' is a folder, it must have a trailing slash ('/'). If 'path' is an "end node" or "vault file", then it
|
107
|
+
# should not have a trailing slash ('/')
|
108
|
+
##
|
109
|
+
def describe!(path, resultHash = nil)
|
110
|
+
|
111
|
+
CerberusClient::Log.instance.debug("VaultClient::describe!(#{path})")
|
112
|
+
|
113
|
+
if(resultHash == nil)
|
114
|
+
resultHash = Hash.new()
|
115
|
+
end
|
116
|
+
|
117
|
+
curChildren = list(path)
|
118
|
+
|
119
|
+
# if curChildren is nil, it's possible there are no children or that we don't have access
|
120
|
+
# It's also possible it is the "end" of the path... what Vault calls "the file"
|
121
|
+
# in that case, we should send back the keys in that path so give it a shot
|
122
|
+
if(curChildren.nil?)
|
123
|
+
resultHash[path] = Array.new
|
124
|
+
readPathAndIterateOnDataWithProc(path, &(Proc.new { |key, value| resultHash[path] << key }) )
|
125
|
+
return resultHash
|
126
|
+
end
|
127
|
+
|
128
|
+
curChildren.each { |childNode|
|
129
|
+
curLocation = path + childNode
|
130
|
+
# if childNode ends with '/' then we have a directory we need to call into
|
131
|
+
if(childNode.end_with?(SLASH))
|
132
|
+
describe!(curLocation, resultHash)
|
133
|
+
else # it is a "directory" that contains keys
|
134
|
+
resultHash[curLocation] = Array.new
|
135
|
+
readPathAndIterateOnDataWithProc(curLocation, &(Proc.new { |key, value| resultHash[curLocation] << key }) )
|
136
|
+
end
|
137
|
+
}
|
138
|
+
return resultHash
|
139
|
+
end
|
140
|
+
|
141
|
+
private
|
142
|
+
|
143
|
+
|
144
|
+
##
|
145
|
+
# Attempts to execute the proc passed in on every key, value located in the 'data' element read at 'path'
|
146
|
+
##
|
147
|
+
def readPathAndIterateOnDataWithProc(path, &p)
|
148
|
+
jsonResponseBody = JSON.parse(read(path))
|
149
|
+
jsonResponseBody[VAULT_LIST_DATA_KEY].each { |dataMapKey, dataMapValue|
|
150
|
+
p.call(dataMapKey, dataMapValue)
|
151
|
+
}
|
152
|
+
end
|
153
|
+
|
154
|
+
##
|
155
|
+
# Do an http request to Vault using the relative URI passed in
|
156
|
+
##
|
157
|
+
def doVaultHttpGet(relativeUri)
|
158
|
+
|
159
|
+
url = URI(@vaultBaseUrl + relativeUri)
|
160
|
+
|
161
|
+
begin
|
162
|
+
response = CerberusClient::Http.new.doHttp(url,
|
163
|
+
'GET', true, nil,
|
164
|
+
{VAULT_TOKEN_HEADER_KEY =>
|
165
|
+
CerberusClient.getCredentialsFromProvider(@credentialsProvider)})
|
166
|
+
|
167
|
+
rescue Cerberus::Exception::HttpError => ex
|
168
|
+
# Since Vault wants to pass back 400 bad request for both paths we don't have access to
|
169
|
+
# and paths that don't actually exist at all, I'm sending back a specific error so that implementing clients
|
170
|
+
# can at least understand the situation they find themselves in
|
171
|
+
#
|
172
|
+
# This client could actually work around this problem by first getting a list of all paths we have access to and
|
173
|
+
# determining if the path exists in that list. If not, 404 (which is more appropriate than 400).
|
174
|
+
# TODO: implement "list > check for path" work around <-- NOTE: This is a relatively expensive operation
|
175
|
+
|
176
|
+
if(!ex.response.nil? && (ex.response.code.to_i == 400) && (hasPermissionErrors?(ex.response.body)))
|
177
|
+
raise Exception::AmbiguousVaultBadRequest.new
|
178
|
+
else
|
179
|
+
raise ex
|
180
|
+
end
|
181
|
+
|
182
|
+
end
|
183
|
+
|
184
|
+
response
|
185
|
+
|
186
|
+
end # doVaultHttp
|
187
|
+
|
188
|
+
##
|
189
|
+
# Parse out the permission errors json
|
190
|
+
##
|
191
|
+
def hasPermissionErrors?(jsonErrorsMsg)
|
192
|
+
begin
|
193
|
+
json = JSON.parse(jsonErrorsMsg)
|
194
|
+
json[VAULT_ERRORS_KEY].each { |err|
|
195
|
+
if(err == VAULT_PERMISSION_DENIED_ERR)
|
196
|
+
return true
|
197
|
+
end
|
198
|
+
}
|
199
|
+
rescue => ex
|
200
|
+
CerberusClient::Log.instance.warn(
|
201
|
+
"VaultClient::hasPermissionErrors? called and exception thrown parsing #{jsonErrorsMsg}: #{ex.message}")
|
202
|
+
return false
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
end
|
207
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require_relative('cerberus_client/version')
|
2
|
+
require_relative('cerberus/vault_client')
|
3
|
+
require_relative('cerberus/default_url_resolver')
|
4
|
+
require_relative('cerberus/default_credentials_provider_chain')
|
5
|
+
require_relative('cerberus/assumed_role_credentials_provider_chain')
|
6
|
+
|
7
|
+
module CerberusClient
|
8
|
+
|
9
|
+
##
|
10
|
+
# Get the vault client using the default vaultUrlResolver and default credentialsProviderChain
|
11
|
+
##
|
12
|
+
def self.getDefaultVaultClient()
|
13
|
+
vaultUrlResolver = Cerberus::DefaultUrlResolver.new
|
14
|
+
return Cerberus::VaultClient.new(vaultUrlResolver,
|
15
|
+
Cerberus::DefaultCredentialsProviderChain.new(vaultUrlResolver))
|
16
|
+
end
|
17
|
+
|
18
|
+
##
|
19
|
+
# Get the vault client using the provided vaultUrlResolver and the credentialsProviderChain
|
20
|
+
##
|
21
|
+
def self.getVaultClientWithUrlResolver(vaultUrlResolver)
|
22
|
+
return Cerberus::VaultClient.new(vaultUrlResolver, Cerberus::DefaultCredentialsProviderChain.new(vaultUrlResolver))
|
23
|
+
end
|
24
|
+
|
25
|
+
##
|
26
|
+
# Get the vault client using the provided vaultUrlResolver and the credentialsProviderChain
|
27
|
+
##
|
28
|
+
def self.getVaultClient(vaultUrlResolver, credentialsProviderChain)
|
29
|
+
return Cerberus::VaultClient.new(vaultUrlResolver, credentialsProviderChain)
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.getVaultClientForAssumedRole(vaultUrlResolver, roleName, roleRegion, roleAccountId)
|
33
|
+
return Cerberus::VaultClient.new(vaultUrlResolver, Cerberus::AssumedRoleCredentialsProviderChain.new(vaultUrlResolver,
|
34
|
+
nil,
|
35
|
+
roleName,
|
36
|
+
roleRegion,
|
37
|
+
roleAccountId))
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
##
|
42
|
+
# Get credentials from implementing credentialProvider
|
43
|
+
##
|
44
|
+
def self.getCredentialsFromProvider(credentialProvider)
|
45
|
+
return credentialProvider.getClientToken
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
# Get credentials provider from chain implementing get getCredentialsProvider
|
50
|
+
##
|
51
|
+
def self.getCredentialsProviderFromChain(credentialProviderChain)
|
52
|
+
return credentialProviderChain.getCredentialsProvider
|
53
|
+
end
|
54
|
+
|
55
|
+
##
|
56
|
+
# Get url from implementing url resolver
|
57
|
+
##
|
58
|
+
def self.getUrlFromResolver(vaultUrlResolver)
|
59
|
+
return vaultUrlResolver.getUrl
|
60
|
+
end
|
61
|
+
|
62
|
+
end # CerberusClient module
|
@@ -0,0 +1,64 @@
|
|
1
|
+
|
2
|
+
module CerberusClient
|
3
|
+
|
4
|
+
require('logger')
|
5
|
+
|
6
|
+
##
|
7
|
+
# Instantiated by the Log singleton
|
8
|
+
# can be replaced by the user provided the Logger supports the four log level outputs
|
9
|
+
##
|
10
|
+
class DefaultLogger
|
11
|
+
|
12
|
+
##
|
13
|
+
# Init the default logger
|
14
|
+
##
|
15
|
+
def initialize
|
16
|
+
@logger = Logger.new STDOUT
|
17
|
+
# log level should be configurable
|
18
|
+
@logger.level = Logger::DEBUG
|
19
|
+
@logger.formatter = proc do |severity, datetime, progname, msg|
|
20
|
+
|
21
|
+
severityFormatted = case severity
|
22
|
+
when "ERROR"
|
23
|
+
"\e[31m#{severity}\e[0m"
|
24
|
+
when "WARN"
|
25
|
+
"\e[33m#{severity}\e[0m"
|
26
|
+
when "DEBUG"
|
27
|
+
"\e[37m#{severity}\e[0m"
|
28
|
+
else
|
29
|
+
"#{severity}"
|
30
|
+
end
|
31
|
+
|
32
|
+
"#{datetime.strftime('%Y-%m-%d %H:%M:%S.%L')} #{severityFormatted}: #{msg}\n"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
##
|
37
|
+
# Log a error message to the default logger
|
38
|
+
##
|
39
|
+
def error(msg)
|
40
|
+
@logger.error(msg)
|
41
|
+
end
|
42
|
+
|
43
|
+
##
|
44
|
+
# Log a warning message to the default logger
|
45
|
+
##
|
46
|
+
def warn(msg)
|
47
|
+
@logger.warn(msg)
|
48
|
+
end
|
49
|
+
|
50
|
+
##
|
51
|
+
# Log a info message to the default logger
|
52
|
+
##
|
53
|
+
def info(msg)
|
54
|
+
@logger.info(msg)
|
55
|
+
end
|
56
|
+
|
57
|
+
##
|
58
|
+
# Log a debug message to the default logger
|
59
|
+
##
|
60
|
+
def debug(msg)
|
61
|
+
@logger.debug(msg)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
|
2
|
+
module CerberusClient
|
3
|
+
|
4
|
+
require_relative('../cerberus/exception/http_error')
|
5
|
+
require('net/http')
|
6
|
+
require_relative('log')
|
7
|
+
|
8
|
+
##
|
9
|
+
#
|
10
|
+
##
|
11
|
+
class Http
|
12
|
+
|
13
|
+
##
|
14
|
+
# Generic HTTP handler for Cerberus Client needs
|
15
|
+
# uri: the fully qualified URI object to call
|
16
|
+
# method: the HTTP method to use'
|
17
|
+
# useSSL: boolean - use HTTPS or not
|
18
|
+
# jsonData: if not nil, made the request body and 'Content-Type' header is set to "application/json"
|
19
|
+
# headers: if not nil, should be a HashMap of header Key, Values
|
20
|
+
##
|
21
|
+
def doHttp(uri, method, useSSL, jsonData = nil, headersMap = nil)
|
22
|
+
|
23
|
+
begin
|
24
|
+
CerberusClient::Log.instance.debug("Http::doHttp -> uri: #{uri}, method: #{method}, useSSL: #{useSSL}, jsonData: #{jsonData}")
|
25
|
+
|
26
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
27
|
+
|
28
|
+
request =
|
29
|
+
case method
|
30
|
+
when 'GET'
|
31
|
+
Net::HTTP::Get.new(uri.request_uri)
|
32
|
+
when 'POST'
|
33
|
+
Net::HTTP::Post.new(uri.request_uri)
|
34
|
+
else
|
35
|
+
raise NotImplementedError
|
36
|
+
end
|
37
|
+
|
38
|
+
if(jsonData != nil); request.body = "#{jsonData}"; request['Content-Type'] = "application/json"; end
|
39
|
+
|
40
|
+
if(headersMap != nil); headersMap.each{ |headerKey, headerValue| request[headerKey] = headerValue } end
|
41
|
+
|
42
|
+
http.use_ssl = useSSL
|
43
|
+
response = http.request(request)
|
44
|
+
|
45
|
+
# this is just for convenience handling down the stack... response object inclucded with the exception
|
46
|
+
if(response.code.to_i < 200 || response.code.to_i >= 300)
|
47
|
+
raise Cerberus::Exception::HttpError.new(
|
48
|
+
"Http response code is non-2xx value: #{response.code}, #{response.body}",
|
49
|
+
response)
|
50
|
+
end
|
51
|
+
|
52
|
+
return response
|
53
|
+
|
54
|
+
rescue => ex
|
55
|
+
# log a warning
|
56
|
+
Log.instance.warn("Exception executing http: #{ex.message}, ex.class #{ex.class}")
|
57
|
+
|
58
|
+
# check to see if we threw the Http error with a response object
|
59
|
+
response = (ex.instance_of?(Cerberus::Exception::HttpError)) ? ex.response : nil
|
60
|
+
|
61
|
+
# raise a specific error that some policy can be enforced on
|
62
|
+
raise Cerberus::Exception::HttpError.new(ex.message, response)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module CerberusClient
|
2
|
+
|
3
|
+
require_relative('default_logger')
|
4
|
+
require('singleton')
|
5
|
+
|
6
|
+
##
|
7
|
+
# Singleton providing logging capabilities for the Cerberus Client
|
8
|
+
# Users can setup their own logger by calling Log.instance.setLoggingProvider
|
9
|
+
# and impelmenting the four log level output methods
|
10
|
+
##
|
11
|
+
class Log
|
12
|
+
include Singleton
|
13
|
+
|
14
|
+
attr_reader :logProvider
|
15
|
+
|
16
|
+
##
|
17
|
+
# Called by Singleton to setup our instance - default logger instantiated
|
18
|
+
# can be replaced by the user
|
19
|
+
##
|
20
|
+
def initialize
|
21
|
+
@logProvider = DefaultLogger.new
|
22
|
+
end
|
23
|
+
|
24
|
+
##
|
25
|
+
# Set the logger to be used by Cerberus Client
|
26
|
+
##
|
27
|
+
def setLoggingProvider(logProvider)
|
28
|
+
@logProvider = logProvider
|
29
|
+
end
|
30
|
+
|
31
|
+
##
|
32
|
+
# Log a error message to the default logger
|
33
|
+
##
|
34
|
+
def error(msg)
|
35
|
+
@logProvider.error(msg)
|
36
|
+
end
|
37
|
+
|
38
|
+
##
|
39
|
+
# Log a warning message to the default logger
|
40
|
+
##
|
41
|
+
def warn(msg)
|
42
|
+
@logProvider.warn(msg)
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Log a info message to the default logger
|
47
|
+
##
|
48
|
+
def info(msg)
|
49
|
+
@logProvider.info(msg)
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
# Log a debug message to the default logger
|
54
|
+
##
|
55
|
+
def debug(msg)
|
56
|
+
@logProvider.debug(msg)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|