cerberus_client 1.5.1 → 2.0.0

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,3 @@
1
+ module CerberusUtils
2
+ VERSION = "2.0.0"
3
+ end
metadata CHANGED
@@ -1,14 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cerberus_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.1
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
+ - Shaun Ford
7
8
  - Joe Teibel
8
9
  autorequire:
9
10
  bindir: exe
10
11
  cert_chain: []
11
- date: 2017-08-25 00:00:00.000000000 Z
12
+ date: 2017-10-17 00:00:00.000000000 Z
12
13
  dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
15
  name: aws-sdk
@@ -52,13 +53,13 @@ dependencies:
52
53
  - - ~>
53
54
  - !ruby/object:Gem::Version
54
55
  version: '1.13'
55
- description: This is a Ruby based client library for communicating with Vault via
56
+ description: This is a Ruby based client library for communicating with Cerberus via
56
57
  HTTP and enables authentication schemes specific to AWS and Cerberus. This client
57
58
  currently supports read-only operations (write operations are not yet implemented,
58
59
  feel free to open a pull request to implement write operations). To learn more about
59
60
  Cerberus, please visit the Cerberus website.
60
61
  email:
61
- - joe.teibel@nike.com
62
+ - shaun.ford@nike.com
62
63
  executables: []
63
64
  extensions: []
64
65
  extra_rdoc_files: []
@@ -73,9 +74,10 @@ files:
73
74
  - README.md
74
75
  - cerberus_client.gemspec
75
76
  - lib/cerberus/assumed_role_credentials_provider_chain.rb
77
+ - lib/cerberus/aws_assumed_role_credentials_provider.rb
76
78
  - lib/cerberus/aws_principal_credentials_provider.rb
77
- - lib/cerberus/aws_role_credentials_provider.rb
78
- - lib/cerberus/aws_role_info.rb
79
+ - lib/cerberus/cerberus_auth_info.rb
80
+ - lib/cerberus/cerberus_client.rb
79
81
  - lib/cerberus/cerberus_client_token.rb
80
82
  - lib/cerberus/default_credentials_provider_chain.rb
81
83
  - lib/cerberus/default_url_resolver.rb
@@ -84,12 +86,12 @@ files:
84
86
  - lib/cerberus/exception/http_error.rb
85
87
  - lib/cerberus/exception/no_valid_providers.rb
86
88
  - lib/cerberus/exception/no_value_error.rb
87
- - lib/cerberus/vault_client.rb
88
89
  - lib/cerberus_client.rb
89
- - lib/cerberus_client/default_logger.rb
90
- - lib/cerberus_client/http.rb
91
- - lib/cerberus_client/log.rb
92
- - lib/cerberus_client/version.rb
90
+ - lib/cerberus_utils/default_logger.rb
91
+ - lib/cerberus_utils/http.rb
92
+ - lib/cerberus_utils/log.rb
93
+ - lib/cerberus_utils/utils.rb
94
+ - lib/cerberus_utils/version.rb
93
95
  homepage: https://github.com/Nike-Inc/cerberus-ruby-client
94
96
  licenses:
95
97
  - Apache License Version 2
@@ -1,239 +0,0 @@
1
- module Cerberus
2
-
3
- require_relative('../cerberus_client/log')
4
- require_relative('../cerberus_client/http')
5
- require_relative('../cerberus_client')
6
- require_relative('exception/no_value_error')
7
- require_relative('exception/http_error')
8
- require_relative('cerberus_client_token')
9
- require_relative('aws_role_info')
10
- require('aws-sdk')
11
- require('net/http')
12
- require('json')
13
- require('base64')
14
-
15
- ##
16
- # The AWS IAM role credentials provider
17
- ##
18
- class AwsRoleCredentialsProvider
19
-
20
- # AWS metadata instance URL
21
- INSTANCE_METADATA_SVC_BASE_URL = "http://169.254.169.254/latest/meta-data"
22
- # relative URI to look up AZ in AWS metadata svc
23
- REGION_REL_URI = "/placement/availability-zone"
24
- # relative URI to look up IAM role in AWS metadata svc
25
- IAM_ROLE_INFO_REL_URI = "/iam/info"
26
- # reference into the metadata data json we get to look up IAM role
27
- IAM_ROLE_ARN_KEY = 'InstanceProfileArn'
28
- # magic number is the index into a split role ARN to grab the acccount ID
29
- ROLE_ARN_ARRAY_INDEX_OF_ACCOUNTNUM = 4
30
- # magic number is the index into a split role ARN to grab the role name
31
- ROLE_ARN_ARRAY_INDEX_OF_ROLENAME = 1
32
- # relative URI to get encrypted auth data from Cerberus
33
- ROLE_AUTH_REL_URI = "/v1/auth/iam-role"
34
- # reference into the decrypted auth data json we get from Cerberus
35
- CERBERUS_AUTH_DATA_CLIENT_TOKEN_KEY = "client_token"
36
- CERBERUS_AUTH_DATA_LEASE_DURATION_KEY = "lease_duration"
37
- CERBERUS_AUTH_DATA_POLICIES_KEY = "policies"
38
-
39
- LOGGER = CerberusClient::Log.instance
40
-
41
- ##
42
- # Init AWS role provider - needs vault base url. Instance metadata service url is optional to make unit tests
43
- # easier and so we can provide a hook to set this via config as needed
44
- ##
45
- def initialize(vaultBaseUrl, instanceMdSvcBaseUrl = nil, roleName = nil, roleRegion = nil, roleAccountId = nil)
46
- @vaultBaseUrl = vaultBaseUrl
47
- @clientToken = nil
48
- @role = get_role_info(instanceMdSvcBaseUrl, roleName, roleRegion, roleAccountId)
49
-
50
- LOGGER.debug("AwsRoleCredentialsProvider initialized with vault base url #{@vaultBaseUrl}")
51
- end
52
-
53
- ##
54
- # Get credentials using AWS IAM role
55
- ##
56
- def getClientToken
57
-
58
- if (@role.nil?)
59
- raise Cerberus::Exception::NoValueError
60
- end
61
-
62
- if (@clientToken.nil?)
63
- @clientToken = getCredentialsFromCerberus
64
- end
65
-
66
- # using two if statements here just to make the logging easier..
67
- # the above we expect on startup, expiration is an interesting event worth a debug log all its own
68
- if (@clientToken.expired?)
69
- LOGGER.debug("Existing ClientToken has expired - refreshing from Cerberus...")
70
- @clientToken = getCredentialsFromCerberus
71
- end
72
-
73
- return @clientToken.authToken
74
-
75
- end
76
-
77
- ##
78
- # Policy: if we are given these three pieces of data, we will assume a role to do auth
79
- ##
80
- def should_assume_role?(roleAccountId, roleName, roleRegion)
81
- !(roleName.nil? || roleAccountId.nil? || roleRegion.nil?)
82
- end
83
-
84
- ##
85
- # Policy: if we do not have an instance MD service URL and we can't assume a role, then this instance
86
- # of the provider cannot use a role to provide credentials. Primarily used for testing.
87
- ##
88
- def have_access_to_role?(instanceMdSvcBaseUrl, roleName, roleRegion, roleAccountId)
89
- (!instanceMdSvcBaseUrl.nil? || should_assume_role?(roleName, roleRegion, roleAccountId))
90
- end
91
-
92
- private
93
-
94
- ##
95
- # Uses provided data to determine how to construct the AwsRoleInfo use by this provider
96
- ##
97
- def get_role_info(instanceMdSvcBaseUrl, roleName, roleRegion, roleAccountId)
98
-
99
- # if we have no metedata about how to auth, we do nothing
100
- # this is used in unit testing primarily
101
- if (!have_access_to_role?(instanceMdSvcBaseUrl, roleName, roleRegion, roleAccountId))
102
- return nil;
103
- elsif (should_assume_role?(roleAccountId, roleName, roleRegion))
104
- # we are assuming a role to do auth
105
- return get_role_from_provided_info(roleName, roleRegion, roleAccountId)
106
- else
107
- # we are using a role that the instance has associated with it
108
- @instanceMdSvcBaseUrl = instanceMdSvcBaseUrl.nil? ? INSTANCE_METADATA_SVC_BASE_URL : instanceMdSvcBaseUrl
109
-
110
- # collect instance MD we need to auth with Cerberus
111
- return get_role_from_instance_metadata
112
- end
113
- end
114
-
115
-
116
- ##
117
- # Get an AwsRoleInfo object from the provided data
118
- ##
119
- def get_role_from_provided_info(roleName, roleRegion, roleAccountId)
120
-
121
- role_creds = Aws::AssumeRoleCredentials.new(
122
- client: Aws::STS::Client.new(region: roleRegion),
123
- role_arn: "arn:aws:iam::#{roleAccountId}:role/#{roleName}",
124
- role_session_name: "hiera-cpe-build")
125
-
126
- return AwsRoleInfo.new(roleName, roleRegion, roleAccountId, credentials: role_creds)
127
- end
128
-
129
- ##
130
- # Use the instance metadata to extract the role information
131
- # This function should only be called from an EC2 instance otherwise the http
132
- # call will fail.
133
- ##
134
- def get_role_from_instance_metadata
135
- role_arn = getIAMRoleARN
136
- region = getRegionFromAZ(getAvailabilityZone)
137
- account_id = getAccountIdFromRoleARN(role_arn)
138
- role_name = getRoleNameFromRoleARN(role_arn)
139
-
140
- LOGGER.debug("roleARN #{role_arn}")
141
- LOGGER.debug("region #{region}")
142
- LOGGER.debug("accountId #{account_id}")
143
- LOGGER.debug("roleName #{role_name}")
144
-
145
- return AwsRoleInfo.new(role_name, region, account_id, nil)
146
- end
147
-
148
- ##
149
- # Reach out to the Cerberus management service and get an auth token
150
- ##
151
- def getCredentialsFromCerberus
152
- begin
153
- authData = doAuthWithCerberus(@role.account_id, @role.name, @role.region)
154
-
155
- LOGGER.debug("Got auth data from Cerberus. Attempting to decrypt...")
156
-
157
- # decrypt the data we got from cerberus to get the vault token
158
- kms = Aws::KMS::Client.new(region: @role.region, credentials: @role.credentials[:credentials])
159
-
160
- decryptedAuthDataJson = JSON.parse(kms.decrypt(ciphertext_blob: Base64.decode64(authData)).plaintext)
161
-
162
- LOGGER.debug("Decrypt successful. Passing back Cerberus auth token.")
163
- # pass back a credentials object that will allow us to reuse it until it expires
164
- CerberusClientToken.new(decryptedAuthDataJson[CERBERUS_AUTH_DATA_CLIENT_TOKEN_KEY],
165
- decryptedAuthDataJson[CERBERUS_AUTH_DATA_LEASE_DURATION_KEY],
166
- decryptedAuthDataJson[CERBERUS_AUTH_DATA_POLICIES_KEY])
167
-
168
- rescue Cerberus::Exception::HttpError
169
- # catch http errors here and assert no value
170
- # this may not actually be the case, there are legitimate reasons HTTP can fail when it "should" work
171
- # but this is handled by logging - a warning is set in the log in during the HTTP call
172
- raise Cerberus::Exception::NoValueError
173
- end
174
- end
175
-
176
- ##
177
- # Get the AWS account ID from the role ARN
178
- # Expects formatting [some value]:[some value]:[some value]::[account id]
179
- ##
180
- def getAccountIdFromRoleARN(roleARN)
181
- roleARN.split(':')[ROLE_ARN_ARRAY_INDEX_OF_ACCOUNTNUM]
182
- end
183
-
184
- ##
185
- # Get the role name from the role ARN
186
- # Expects formatting [some value]:[some value]:[some value]::[account id]
187
- ##
188
- def getRoleNameFromRoleARN(roleARN)
189
- roleARN.split('/')[ROLE_ARN_ARRAY_INDEX_OF_ROLENAME]
190
- end
191
-
192
- ##
193
- # Read the IAM role ARN from the instance metadata
194
- # Will throw an HTTP exception if there is no IAM role associated with the instance
195
- ##
196
- def getIAMRoleARN
197
- response = doHttpToMDService(IAM_ROLE_INFO_REL_URI)
198
- jsonResponseBody = JSON.parse(response.body)
199
- jsonResponseBody[IAM_ROLE_ARN_KEY]
200
- end
201
-
202
- ##
203
- # Get the region from AWS instance metadata
204
- ##
205
- def getAvailabilityZone
206
- doHttpToMDService(REGION_REL_URI).body
207
- end
208
-
209
- ##
210
- # Get region from AZ
211
- ##
212
- def getRegionFromAZ(az)
213
- az[0, az.length-1]
214
- end
215
-
216
- ##
217
- # Call the instance metadata service with a relative URI and return the response if the call succeeds
218
- # else throw an IOError for non-2xx responses and RuntimeError for any exceptions down the stack
219
- ##
220
- def doHttpToMDService(relUri)
221
- url = URI(@instanceMdSvcBaseUrl + relUri)
222
- CerberusClient::Http.new.doHttp(url, 'GET', false)
223
- end
224
-
225
- ##
226
- #
227
- ##
228
- def doAuthWithCerberus(accountId, roleName, region)
229
- postJsonData = JSON.generate({:account_id => accountId, :role_name => roleName, :region => region})
230
- authUrl = URI(@vaultBaseUrl + ROLE_AUTH_REL_URI)
231
- useSSL = ! ("#{@vaultBaseUrl}".include? "localhost")
232
- authResponse = CerberusClient::Http.new.doHttp(authUrl, 'POST', useSSL, postJsonData)
233
- # if we got this far, we should have a valid response with encrypted data
234
- # send back the encrypted data
235
- JSON.parse(authResponse.body)['auth_data']
236
- end
237
-
238
- end
239
- end
@@ -1,15 +0,0 @@
1
- module Cerberus
2
- class AwsRoleInfo
3
- attr_reader :name
4
- attr_reader :region
5
- attr_reader :credentials
6
- attr_reader :account_id
7
-
8
- def initialize(name, region, account_id, credentials = nil)
9
- @name = name
10
- @region = region
11
- @account_id = account_id
12
- @credentials = credentials
13
- end
14
- end
15
- end
@@ -1,207 +0,0 @@
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
- begin
46
- response = doVaultHttpGet(SECRET_PATH_PREFIX + path)
47
- CerberusClient::Log.instance.debug("VaultClient::read(path) HTTP response: #{response.code}, #{response.message}")
48
- response.body
49
-
50
- rescue => ex
51
- CerberusClient::Log.instance.error("VaultClient::read(#{path}) unhandled exception trying to read: #{ex.message}")
52
- raise ex
53
- end
54
- end # read
55
-
56
- ##
57
- # Read operation for a specified path.
58
- ##
59
- def readKey(path, key)
60
- begin
61
- CerberusClient::Log.instance.error("VaultClient::read(#{path}, #{key})")
62
-
63
- readPathAndIterateOnDataWithProc(path, &(Proc.new { |k, v| if(key == k); return v; end }) )
64
-
65
- # else, we didn't find it
66
- return nil
67
-
68
- rescue => ex
69
- CerberusClient::Log.instance.error("VaultClient::read(#{path}, #{key}) unhandled exception trying to read: #{ex.message}")
70
- raise ex
71
- end
72
- end
73
-
74
- ##
75
- # Returns a list of key names at the specified location. Folders are suffixed with /.
76
- # The input must be a folder; list on a file will return nil
77
- ##
78
- def list(path)
79
- begin
80
- response = doVaultHttpGet(SECRET_PATH_PREFIX + path + "?list=true")
81
-
82
- CerberusClient::Log.instance.debug("VaultClient::list(#{path}) HTTP response: #{response.code}, #{response.message}")
83
-
84
- jsonResonseBody = JSON.parse(response.body)
85
- pathList = jsonResonseBody[VAULT_LIST_DATA_KEY][VAULT_LIST_KEYS_KEY]
86
- CerberusClient::Log.instance.debug("VaultClient::list returning #{pathList.join(", ")} ")
87
- pathList
88
-
89
- rescue => ex
90
-
91
- # check to see if we threw the Http error with a response object
92
- response = (ex.instance_of?(Cerberus::Exception::HttpError)) ? ex.response : nil
93
- if(!response.nil? && response.code.to_i == 404)
94
- return nil
95
- end
96
-
97
- CerberusClient::Log.instance.error("VaultClient::list(#{path}) unhandled exception trying to read: #{ex.message}")
98
- raise ex
99
- end
100
- end
101
-
102
- ##
103
- # This is potentially an expensive operation depending on the depth of the tree we're trying to parse
104
- # It recursively walks 'path' and returns a hash of all child [path] => [array of keys] found under 'path'
105
- # if 'path' is a folder, it must have a trailing slash ('/'). If 'path' is an "end node" or "vault file", then it
106
- # should not have a trailing slash ('/')
107
- ##
108
- def describe!(path, resultHash = nil)
109
-
110
- CerberusClient::Log.instance.debug("VaultClient::describe!(#{path})")
111
-
112
- if(resultHash == nil)
113
- resultHash = Hash.new()
114
- end
115
-
116
- curChildren = list(path)
117
-
118
- # if curChildren is nil, it's possible there are no children or that we don't have access
119
- # It's also possible it is the "end" of the path... what Vault calls "the file"
120
- # in that case, we should send back the keys in that path so give it a shot
121
- if(curChildren.nil?)
122
- resultHash[path] = Array.new
123
- readPathAndIterateOnDataWithProc(path, &(Proc.new { |key, value| resultHash[path] << key }) )
124
- return resultHash
125
- end
126
-
127
- curChildren.each { |childNode|
128
- curLocation = path + childNode
129
- # if childNode ends with '/' then we have a directory we need to call into
130
- if(childNode.end_with?(SLASH))
131
- describe!(curLocation, resultHash)
132
- else # it is a "directory" that contains keys
133
- resultHash[curLocation] = Array.new
134
- readPathAndIterateOnDataWithProc(curLocation, &(Proc.new { |key, value| resultHash[curLocation] << key }) )
135
- end
136
- }
137
- return resultHash
138
- end
139
-
140
- private
141
-
142
-
143
- ##
144
- # Attempts to execute the proc passed in on every key, value located in the 'data' element read at 'path'
145
- ##
146
- def readPathAndIterateOnDataWithProc(path, &p)
147
- jsonResponseBody = JSON.parse(read(path))
148
- jsonResponseBody[VAULT_LIST_DATA_KEY].each { |dataMapKey, dataMapValue|
149
- p.call(dataMapKey, dataMapValue)
150
- }
151
- end
152
-
153
- ##
154
- # Do an http request to Vault using the relative URI passed in
155
- ##
156
- def doVaultHttpGet(relativeUri)
157
-
158
- url = URI(@vaultBaseUrl + relativeUri)
159
- useSSL = ! ("#{@vaultBaseUrl}".include? "localhost")
160
-
161
- begin
162
- response = CerberusClient::Http.new.doHttp(url,
163
- 'GET', useSSL, 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