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.
@@ -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