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