googleauth 0.17.1 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +112 -62
  3. data/README.md +49 -14
  4. data/lib/googleauth/application_default.rb +11 -26
  5. data/lib/googleauth/base_client.rb +80 -0
  6. data/lib/googleauth/client_id.rb +10 -25
  7. data/lib/googleauth/compute_engine.rb +10 -25
  8. data/lib/googleauth/credentials.rb +12 -27
  9. data/lib/googleauth/credentials_loader.rb +27 -43
  10. data/lib/googleauth/default_credentials.rb +15 -25
  11. data/lib/googleauth/external_account/aws_credentials.rb +378 -0
  12. data/lib/googleauth/external_account/base_credentials.rb +158 -0
  13. data/lib/googleauth/external_account/external_account_utils.rb +103 -0
  14. data/lib/googleauth/external_account/identity_pool_credentials.rb +118 -0
  15. data/lib/googleauth/external_account/pluggable_credentials.rb +156 -0
  16. data/lib/googleauth/external_account.rb +93 -0
  17. data/lib/googleauth/helpers/connection.rb +35 -0
  18. data/lib/googleauth/iam.rb +10 -25
  19. data/lib/googleauth/id_tokens/errors.rb +9 -23
  20. data/lib/googleauth/id_tokens/key_sources.rb +19 -33
  21. data/lib/googleauth/id_tokens/verifier.rb +9 -23
  22. data/lib/googleauth/id_tokens.rb +11 -25
  23. data/lib/googleauth/json_key_reader.rb +10 -25
  24. data/lib/googleauth/oauth2/sts_client.rb +109 -0
  25. data/lib/googleauth/scope_util.rb +10 -25
  26. data/lib/googleauth/service_account.rb +11 -28
  27. data/lib/googleauth/signet.rb +16 -58
  28. data/lib/googleauth/stores/file_token_store.rb +10 -25
  29. data/lib/googleauth/stores/redis_token_store.rb +10 -25
  30. data/lib/googleauth/token_store.rb +10 -25
  31. data/lib/googleauth/user_authorizer.rb +10 -25
  32. data/lib/googleauth/user_refresh.rb +15 -27
  33. data/lib/googleauth/version.rb +11 -26
  34. data/lib/googleauth/web_user_authorizer.rb +10 -25
  35. data/lib/googleauth.rb +10 -25
  36. metadata +26 -11
@@ -1,31 +1,16 @@
1
- # Copyright 2017, Google Inc.
2
- # All rights reserved.
1
+ # Copyright 2017 Google, Inc.
3
2
  #
4
- # Redistribution and use in source and binary forms, with or without
5
- # modification, are permitted provided that the following conditions are
6
- # met:
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
7
6
  #
8
- # * Redistributions of source code must retain the above copyright
9
- # notice, this list of conditions and the following disclaimer.
10
- # * Redistributions in binary form must reproduce the above
11
- # copyright notice, this list of conditions and the following disclaimer
12
- # in the documentation and/or other materials provided with the
13
- # distribution.
14
- # * Neither the name of Google Inc. nor the names of its
15
- # contributors may be used to endorse or promote products derived from
16
- # this software without specific prior written permission.
7
+ # http://www.apache.org/licenses/LICENSE-2.0
17
8
  #
18
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19
- # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20
- # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21
- # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22
- # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
- # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
- # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25
- # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26
- # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27
- # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
- # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
29
14
 
30
15
  require "forwardable"
31
16
  require "json"
@@ -370,7 +355,7 @@ module Google
370
355
  @project_id = options["project_id"] || options["project"]
371
356
  @quota_project_id = options["quota_project_id"]
372
357
  case keyfile
373
- when Signet::OAuth2::Client
358
+ when Google::Auth::BaseClient
374
359
  update_from_signet keyfile
375
360
  when Hash
376
361
  update_from_hash keyfile, options
@@ -379,7 +364,7 @@ module Google
379
364
  end
380
365
  CredentialsLoader.warn_if_cloud_sdk_credentials @client.client_id
381
366
  @project_id ||= CredentialsLoader.load_gcloud_project_id
382
- @client.fetch_access_token!
367
+ @client.fetch_access_token! if @client.needs_access_token?
383
368
  @env_vars = nil
384
369
  @paths = nil
385
370
  @scope = nil
@@ -1,33 +1,17 @@
1
- # Copyright 2015, Google Inc.
2
- # All rights reserved.
1
+ # Copyright 2015 Google, Inc.
3
2
  #
4
- # Redistribution and use in source and binary forms, with or without
5
- # modification, are permitted provided that the following conditions are
6
- # met:
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
7
6
  #
8
- # * Redistributions of source code must retain the above copyright
9
- # notice, this list of conditions and the following disclaimer.
10
- # * Redistributions in binary form must reproduce the above
11
- # copyright notice, this list of conditions and the following disclaimer
12
- # in the documentation and/or other materials provided with the
13
- # distribution.
14
- # * Neither the name of Google Inc. nor the names of its
15
- # contributors may be used to endorse or promote products derived from
16
- # this software without specific prior written permission.
7
+ # http://www.apache.org/licenses/LICENSE-2.0
17
8
  #
18
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19
- # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20
- # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21
- # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22
- # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
- # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
- # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25
- # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26
- # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27
- # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
- # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
-
30
- require "memoist"
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
31
15
  require "os"
32
16
  require "rbconfig"
33
17
 
@@ -38,7 +22,6 @@ module Google
38
22
  # CredentialsLoader contains the behaviour used to locate and find default
39
23
  # credentials files on the file system.
40
24
  module CredentialsLoader
41
- extend Memoist
42
25
  ENV_VAR = "GOOGLE_APPLICATION_CREDENTIALS".freeze
43
26
  PRIVATE_KEY_VAR = "GOOGLE_PRIVATE_KEY".freeze
44
27
  CLIENT_EMAIL_VAR = "GOOGLE_CLIENT_EMAIL".freeze
@@ -47,29 +30,30 @@ module Google
47
30
  REFRESH_TOKEN_VAR = "GOOGLE_REFRESH_TOKEN".freeze
48
31
  ACCOUNT_TYPE_VAR = "GOOGLE_ACCOUNT_TYPE".freeze
49
32
  PROJECT_ID_VAR = "GOOGLE_PROJECT_ID".freeze
33
+ AWS_REGION_VAR = "AWS_REGION".freeze
34
+ AWS_DEFAULT_REGION_VAR = "AWS_DEFAULT_REGION".freeze
35
+ AWS_ACCESS_KEY_ID_VAR = "AWS_ACCESS_KEY_ID".freeze
36
+ AWS_SECRET_ACCESS_KEY_VAR = "AWS_SECRET_ACCESS_KEY".freeze
37
+ AWS_SESSION_TOKEN_VAR = "AWS_SESSION_TOKEN".freeze
50
38
  GCLOUD_POSIX_COMMAND = "gcloud".freeze
51
39
  GCLOUD_WINDOWS_COMMAND = "gcloud.cmd".freeze
52
- GCLOUD_CONFIG_COMMAND =
53
- "config config-helper --format json --verbosity none".freeze
40
+ GCLOUD_CONFIG_COMMAND = "config config-helper --format json --verbosity none".freeze
54
41
 
55
42
  CREDENTIALS_FILE_NAME = "application_default_credentials.json".freeze
56
- NOT_FOUND_ERROR =
57
- "Unable to read the credential file specified by #{ENV_VAR}".freeze
43
+ NOT_FOUND_ERROR = "Unable to read the credential file specified by #{ENV_VAR}".freeze
58
44
  WELL_KNOWN_PATH = "gcloud/#{CREDENTIALS_FILE_NAME}".freeze
59
45
  WELL_KNOWN_ERROR = "Unable to read the default credential file".freeze
60
46
 
61
- SYSTEM_DEFAULT_ERROR =
62
- "Unable to read the system default credential file".freeze
47
+ SYSTEM_DEFAULT_ERROR = "Unable to read the system default credential file".freeze
63
48
 
64
- CLOUD_SDK_CLIENT_ID = "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.app"\
65
- "s.googleusercontent.com".freeze
49
+ CLOUD_SDK_CLIENT_ID = "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.app" \
50
+ "s.googleusercontent.com".freeze
66
51
 
67
- CLOUD_SDK_CREDENTIALS_WARNING = "Your application has authenticated using end user "\
68
- "credentials from Google Cloud SDK. We recommend that most server applications use "\
69
- "service accounts instead. If your application continues to use end user credentials "\
70
- 'from Cloud SDK, you might receive a "quota exceeded" or "API not enabled" error. For '\
71
- "more information about service accounts, see "\
72
- "https://cloud.google.com/docs/authentication/. To suppress this message, set the "\
52
+ CLOUD_SDK_CREDENTIALS_WARNING =
53
+ "Your application has authenticated using end user credentials from Google Cloud SDK. We recommend that most " \
54
+ "server applications use service accounts instead. If your application continues to use end user credentials " \
55
+ 'from Cloud SDK, you might receive a "quota exceeded" or "API not enabled" error. For more information about ' \
56
+ "service accounts, see https://cloud.google.com/docs/authentication/. To suppress this message, set the " \
73
57
  "GOOGLE_AUTH_SUPPRESS_CREDENTIALS_WARNINGS environment variable.".freeze
74
58
 
75
59
  # make_creds proxies the construction of a credentials instance
@@ -175,7 +159,7 @@ module Google
175
159
  def load_gcloud_project_id
176
160
  gcloud = GCLOUD_WINDOWS_COMMAND if OS.windows?
177
161
  gcloud = GCLOUD_POSIX_COMMAND unless OS.windows?
178
- gcloud_json = IO.popen("#{gcloud} #{GCLOUD_CONFIG_COMMAND}", &:read)
162
+ gcloud_json = IO.popen("#{gcloud} #{GCLOUD_CONFIG_COMMAND}", in: :close, err: :close, &:read)
179
163
  config = MultiJson.load gcloud_json
180
164
  config["configuration"]["properties"]["core"]["project"]
181
165
  rescue StandardError
@@ -1,31 +1,16 @@
1
- # Copyright 2015, Google Inc.
2
- # All rights reserved.
1
+ # Copyright 2015 Google, Inc.
3
2
  #
4
- # Redistribution and use in source and binary forms, with or without
5
- # modification, are permitted provided that the following conditions are
6
- # met:
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
7
6
  #
8
- # * Redistributions of source code must retain the above copyright
9
- # notice, this list of conditions and the following disclaimer.
10
- # * Redistributions in binary form must reproduce the above
11
- # copyright notice, this list of conditions and the following disclaimer
12
- # in the documentation and/or other materials provided with the
13
- # distribution.
14
- # * Neither the name of Google Inc. nor the names of its
15
- # contributors may be used to endorse or promote products derived from
16
- # this software without specific prior written permission.
7
+ # http://www.apache.org/licenses/LICENSE-2.0
17
8
  #
18
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19
- # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20
- # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21
- # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22
- # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
- # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
- # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25
- # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26
- # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27
- # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
- # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
29
14
 
30
15
  require "multi_json"
31
16
  require "stringio"
@@ -33,6 +18,7 @@ require "stringio"
33
18
  require "googleauth/credentials_loader"
34
19
  require "googleauth/service_account"
35
20
  require "googleauth/user_refresh"
21
+ require "googleauth/external_account"
36
22
 
37
23
  module Google
38
24
  # Module Auth provides classes that provide Google-specific authorization
@@ -68,6 +54,8 @@ module Google
68
54
  ServiceAccountCredentials
69
55
  when "authorized_user"
70
56
  UserRefreshCredentials
57
+ when "external_account"
58
+ ExternalAccount::Credentials
71
59
  else
72
60
  raise "credentials type '#{type}' is not supported"
73
61
  end
@@ -84,6 +72,8 @@ module Google
84
72
  [json_key, ServiceAccountCredentials]
85
73
  when "authorized_user"
86
74
  [json_key, UserRefreshCredentials]
75
+ when "external_account"
76
+ [json_key, ExternalAccount::Credentials]
87
77
  else
88
78
  raise "credentials type '#{type}' is not supported"
89
79
  end
@@ -0,0 +1,378 @@
1
+ # Copyright 2023 Google, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "time"
16
+ require "googleauth/external_account/base_credentials"
17
+ require "googleauth/external_account/external_account_utils"
18
+
19
+ module Google
20
+ # Module Auth provides classes that provide Google-specific authorization used to access Google APIs.
21
+ module Auth
22
+ # Authenticates requests using External Account credentials, such as those provided by the AWS provider.
23
+ module ExternalAccount
24
+ # This module handles the retrieval of credentials from Google Cloud by utilizing the AWS EC2 metadata service and
25
+ # then exchanging the credentials for a short-lived Google Cloud access token.
26
+ class AwsCredentials
27
+ # Constant for imdsv2 session token expiration in seconds
28
+ IMDSV2_TOKEN_EXPIRATION_IN_SECONDS = 300
29
+
30
+ include Google::Auth::ExternalAccount::BaseCredentials
31
+ include Google::Auth::ExternalAccount::ExternalAccountUtils
32
+ extend CredentialsLoader
33
+
34
+ # Will always be nil, but method still gets used.
35
+ attr_reader :client_id
36
+
37
+ def initialize options = {}
38
+ base_setup options
39
+
40
+ @audience = options[:audience]
41
+ @credential_source = options[:credential_source] || {}
42
+ @environment_id = @credential_source[:environment_id]
43
+ @region_url = @credential_source[:region_url]
44
+ @credential_verification_url = @credential_source[:url]
45
+ @regional_cred_verification_url = @credential_source[:regional_cred_verification_url]
46
+ @imdsv2_session_token_url = @credential_source[:imdsv2_session_token_url]
47
+
48
+ # These will be lazily loaded when needed, or will raise an error if not provided
49
+ @region = nil
50
+ @request_signer = nil
51
+ @imdsv2_session_token = nil
52
+ @imdsv2_session_token_expiry = nil
53
+ end
54
+
55
+ # Retrieves the subject token using the credential_source object.
56
+ # The subject token is a serialized [AWS GetCallerIdentity signed request](
57
+ # https://cloud.google.com/iam/docs/access-resources-aws#exchange-token).
58
+ #
59
+ # The logic is summarized as:
60
+ #
61
+ # Retrieve the AWS region from the AWS_REGION or AWS_DEFAULT_REGION environment variable or from the AWS
62
+ # metadata server availability-zone if not found in the environment variable.
63
+ #
64
+ # Check AWS credentials in environment variables. If not found, retrieve from the AWS metadata server
65
+ # security-credentials endpoint.
66
+ #
67
+ # When retrieving AWS credentials from the metadata server security-credentials endpoint, the AWS role needs to
68
+ # be determined by # calling the security-credentials endpoint without any argument.
69
+ # Then the credentials can be retrieved via: security-credentials/role_name
70
+ #
71
+ # Generate the signed request to AWS STS GetCallerIdentity action.
72
+ #
73
+ # Inject x-goog-cloud-target-resource into header and serialize the signed request.
74
+ # This will be the subject-token to pass to GCP STS.
75
+ #
76
+ # @return [string] The retrieved subject token.
77
+ #
78
+ def retrieve_subject_token!
79
+ if @request_signer.nil?
80
+ @region = region
81
+ @request_signer = AwsRequestSigner.new @region
82
+ end
83
+
84
+ request = {
85
+ method: "POST",
86
+ url: @regional_cred_verification_url.sub("{region}", @region)
87
+ }
88
+
89
+ request_options = @request_signer.generate_signed_request fetch_security_credentials, request
90
+
91
+ request_headers = request_options[:headers]
92
+ request_headers["x-goog-cloud-target-resource"] = @audience
93
+
94
+ aws_signed_request = {
95
+ headers: [],
96
+ method: request_options[:method],
97
+ url: request_options[:url]
98
+ }
99
+
100
+ aws_signed_request[:headers] = request_headers.keys.sort.map do |key|
101
+ { key: key, value: request_headers[key] }
102
+ end
103
+
104
+ uri_escape aws_signed_request.to_json
105
+ end
106
+
107
+ private
108
+
109
+ def imdsv2_session_token
110
+ return @imdsv2_session_token unless imdsv2_session_token_invalid?
111
+ raise "IMDSV2 token url must be provided" if @imdsv2_session_token_url.nil?
112
+ begin
113
+ response = connection.put @imdsv2_session_token_url do |req|
114
+ req.headers["x-aws-ec2-metadata-token-ttl-seconds"] = IMDSV2_TOKEN_EXPIRATION_IN_SECONDS.to_s
115
+ end
116
+ rescue Faraday::Error => e
117
+ raise "Fetching AWS IMDSV2 token error: #{e}"
118
+ end
119
+ raise Faraday::Error unless response.success?
120
+ @imdsv2_session_token = response.body
121
+ @imdsv2_session_token_expiry = Time.now + IMDSV2_TOKEN_EXPIRATION_IN_SECONDS
122
+ @imdsv2_session_token
123
+ end
124
+
125
+ def imdsv2_session_token_invalid?
126
+ return true if @imdsv2_session_token.nil?
127
+ @imdsv2_session_token_expiry.nil? || @imdsv2_session_token_expiry < Time.now
128
+ end
129
+
130
+ def get_aws_resource url, name, data: nil, headers: {}
131
+ begin
132
+ headers["x-aws-ec2-metadata-token"] = imdsv2_session_token
133
+ response = if data
134
+ headers["Content-Type"] = "application/json"
135
+ connection.post url, data, headers
136
+ else
137
+ connection.get url, nil, headers
138
+ end
139
+
140
+ raise Faraday::Error unless response.success?
141
+ response
142
+ rescue Faraday::Error
143
+ raise "Failed to retrieve AWS #{name}."
144
+ end
145
+ end
146
+
147
+ def uri_escape string
148
+ if string.nil?
149
+ nil
150
+ else
151
+ CGI.escape(string.encode("UTF-8")).gsub("+", "%20").gsub("%7E", "~")
152
+ end
153
+ end
154
+
155
+ # Retrieves the AWS security credentials required for signing AWS requests from either the AWS security
156
+ # credentials environment variables or from the AWS metadata server.
157
+ def fetch_security_credentials
158
+ env_aws_access_key_id = ENV[CredentialsLoader::AWS_ACCESS_KEY_ID_VAR]
159
+ env_aws_secret_access_key = ENV[CredentialsLoader::AWS_SECRET_ACCESS_KEY_VAR]
160
+ # This is normally not available for permanent credentials.
161
+ env_aws_session_token = ENV[CredentialsLoader::AWS_SESSION_TOKEN_VAR]
162
+
163
+ if env_aws_access_key_id && env_aws_secret_access_key
164
+ return {
165
+ access_key_id: env_aws_access_key_id,
166
+ secret_access_key: env_aws_secret_access_key,
167
+ session_token: env_aws_session_token
168
+ }
169
+ end
170
+
171
+ role_name = fetch_metadata_role_name
172
+ credentials = fetch_metadata_security_credentials role_name
173
+
174
+ {
175
+ access_key_id: credentials["AccessKeyId"],
176
+ secret_access_key: credentials["SecretAccessKey"],
177
+ session_token: credentials["Token"]
178
+ }
179
+ end
180
+
181
+ # Retrieves the AWS role currently attached to the current AWS workload by querying the AWS metadata server.
182
+ # This is needed for the AWS metadata server security credentials endpoint in order to retrieve the AWS security
183
+ # credentials needed to sign requests to AWS APIs.
184
+ def fetch_metadata_role_name
185
+ unless @credential_verification_url
186
+ raise "Unable to determine the AWS metadata server security credentials endpoint"
187
+ end
188
+
189
+ get_aws_resource(@credential_verification_url, "IAM Role").body
190
+ end
191
+
192
+ # Retrieves the AWS security credentials required for signing AWS requests from the AWS metadata server.
193
+ def fetch_metadata_security_credentials role_name
194
+ response = get_aws_resource "#{@credential_verification_url}/#{role_name}", "credentials"
195
+ MultiJson.load response.body
196
+ end
197
+
198
+ def region
199
+ @region = ENV[CredentialsLoader::AWS_REGION_VAR] || ENV[CredentialsLoader::AWS_DEFAULT_REGION_VAR]
200
+
201
+ unless @region
202
+ raise "region_url or region must be set for external account credentials" unless @region_url
203
+
204
+ @region ||= get_aws_resource(@region_url, "region").body[0..-2]
205
+ end
206
+
207
+ @region
208
+ end
209
+ end
210
+
211
+ # Implements an AWS request signer based on the AWS Signature Version 4 signing process.
212
+ # https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
213
+ class AwsRequestSigner
214
+ # Instantiates an AWS request signer used to compute authenticated signed requests to AWS APIs based on the AWS
215
+ # Signature Version 4 signing process.
216
+ #
217
+ # @param [string] region_name
218
+ # The AWS region to use.
219
+ def initialize region_name
220
+ @region_name = region_name
221
+ end
222
+
223
+ # Generates the signed request for the provided HTTP request for calling
224
+ # an AWS API. This follows the steps described at:
225
+ # https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
226
+ #
227
+ # @param [Hash[string, string]] aws_security_credentials
228
+ # A dictionary containing the AWS security credentials.
229
+ # @param [string] url
230
+ # The AWS service URL containing the canonical URI and query string.
231
+ # @param [string] method
232
+ # The HTTP method used to call this API.
233
+ #
234
+ # @return [hash{string => string}]
235
+ # The AWS signed request dictionary object.
236
+ #
237
+ def generate_signed_request aws_credentials, original_request
238
+ uri = Addressable::URI.parse original_request[:url]
239
+ raise "Invalid AWS service URL" unless uri.hostname && uri.scheme == "https"
240
+ service_name = uri.host.split(".").first
241
+
242
+ datetime = Time.now.utc.strftime "%Y%m%dT%H%M%SZ"
243
+ date = datetime[0, 8]
244
+
245
+ headers = aws_headers aws_credentials, original_request, datetime
246
+
247
+ request_payload = original_request[:data] || ""
248
+ content_sha256 = sha256_hexdigest request_payload
249
+
250
+ canonical_req = canonical_request original_request[:method], uri, headers, content_sha256
251
+ sts = string_to_sign datetime, canonical_req, service_name
252
+
253
+ # Authorization header requires everything else to be properly setup in order to be properly
254
+ # calculated.
255
+ headers["Authorization"] = build_authorization_header headers, sts, aws_credentials, service_name, date
256
+
257
+ {
258
+ url: uri.to_s,
259
+ headers: headers,
260
+ method: original_request[:method],
261
+ data: (request_payload unless request_payload.empty?)
262
+ }.compact
263
+ end
264
+
265
+ private
266
+
267
+ def aws_headers aws_credentials, original_request, datetime
268
+ uri = Addressable::URI.parse original_request[:url]
269
+ temp_headers = original_request[:headers] || {}
270
+ headers = {}
271
+ temp_headers.each_key { |k| headers[k.to_s] = temp_headers[k] }
272
+ headers["host"] = uri.host
273
+ headers["x-amz-date"] = datetime
274
+ headers["x-amz-security-token"] = aws_credentials[:session_token] if aws_credentials[:session_token]
275
+ headers
276
+ end
277
+
278
+ def build_authorization_header headers, sts, aws_credentials, service_name, date
279
+ [
280
+ "AWS4-HMAC-SHA256",
281
+ "Credential=#{credential aws_credentials[:access_key_id], date, service_name},",
282
+ "SignedHeaders=#{headers.keys.sort.join ';'},",
283
+ "Signature=#{signature aws_credentials[:secret_access_key], date, sts, service_name}"
284
+ ].join(" ")
285
+ end
286
+
287
+ def signature secret_access_key, date, string_to_sign, service
288
+ k_date = hmac "AWS4#{secret_access_key}", date
289
+ k_region = hmac k_date, @region_name
290
+ k_service = hmac k_region, service
291
+ k_credentials = hmac k_service, "aws4_request"
292
+
293
+ hexhmac k_credentials, string_to_sign
294
+ end
295
+
296
+ def hmac key, value
297
+ OpenSSL::HMAC.digest OpenSSL::Digest.new("sha256"), key, value
298
+ end
299
+
300
+ def hexhmac key, value
301
+ OpenSSL::HMAC.hexdigest OpenSSL::Digest.new("sha256"), key, value
302
+ end
303
+
304
+ def credential access_key_id, date, service
305
+ "#{access_key_id}/#{credential_scope date, service}"
306
+ end
307
+
308
+ def credential_scope date, service
309
+ [
310
+ date,
311
+ @region_name,
312
+ service,
313
+ "aws4_request"
314
+ ].join("/")
315
+ end
316
+
317
+ def string_to_sign datetime, canonical_request, service
318
+ [
319
+ "AWS4-HMAC-SHA256",
320
+ datetime,
321
+ credential_scope(datetime[0, 8], service),
322
+ sha256_hexdigest(canonical_request)
323
+ ].join("\n")
324
+ end
325
+
326
+ def host uri
327
+ # Handles known and unknown URI schemes; default_port nil when unknown.
328
+ if uri.default_port == uri.port
329
+ uri.host
330
+ else
331
+ "#{uri.host}:#{uri.port}"
332
+ end
333
+ end
334
+
335
+ def canonical_request http_method, uri, headers, content_sha256
336
+ headers = headers.sort_by(&:first) # transforms to a sorted array of [key, value]
337
+
338
+ [
339
+ http_method,
340
+ uri.path.empty? ? "/" : uri.path,
341
+ build_canonical_querystring(uri.query || ""),
342
+ headers.map { |k, v| "#{k}:#{v}\n" }.join, # Canonical headers
343
+ headers.map(&:first).join(";"), # Signed headers
344
+ content_sha256
345
+ ].join("\n")
346
+ end
347
+
348
+ def sha256_hexdigest string
349
+ OpenSSL::Digest::SHA256.hexdigest string
350
+ end
351
+
352
+ # Generates the canonical query string given a raw query string.
353
+ # Logic is based on
354
+ # https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
355
+ # Code is from the AWS SDK for Ruby
356
+ # https://github.com/aws/aws-sdk-ruby/blob/0ac3d0a393ed216290bfb5f0383380376f6fb1f1/gems/aws-sigv4/lib/aws-sigv4/signer.rb#L532
357
+ def build_canonical_querystring query
358
+ params = query.split "&"
359
+ params = params.map { |p| p.include?("=") ? p : "#{p}=" }
360
+
361
+ params.each.with_index.sort do |(a, a_offset), (b, b_offset)|
362
+ a_name, a_value = a.split "="
363
+ b_name, b_value = b.split "="
364
+ if a_name == b_name
365
+ if a_value == b_value
366
+ a_offset <=> b_offset
367
+ else
368
+ a_value <=> b_value
369
+ end
370
+ else
371
+ a_name <=> b_name
372
+ end
373
+ end.map(&:first).join("&")
374
+ end
375
+ end
376
+ end
377
+ end
378
+ end