cerberus_client 1.2.1

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