googleauth 0.17.1 → 1.7.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +112 -62
- data/README.md +49 -14
- data/lib/googleauth/application_default.rb +11 -26
- data/lib/googleauth/base_client.rb +80 -0
- data/lib/googleauth/client_id.rb +10 -25
- data/lib/googleauth/compute_engine.rb +10 -25
- data/lib/googleauth/credentials.rb +12 -27
- data/lib/googleauth/credentials_loader.rb +27 -43
- data/lib/googleauth/default_credentials.rb +15 -25
- data/lib/googleauth/external_account/aws_credentials.rb +378 -0
- data/lib/googleauth/external_account/base_credentials.rb +158 -0
- data/lib/googleauth/external_account/external_account_utils.rb +103 -0
- data/lib/googleauth/external_account/identity_pool_credentials.rb +118 -0
- data/lib/googleauth/external_account/pluggable_credentials.rb +156 -0
- data/lib/googleauth/external_account.rb +93 -0
- data/lib/googleauth/helpers/connection.rb +35 -0
- data/lib/googleauth/iam.rb +10 -25
- data/lib/googleauth/id_tokens/errors.rb +9 -23
- data/lib/googleauth/id_tokens/key_sources.rb +19 -33
- data/lib/googleauth/id_tokens/verifier.rb +9 -23
- data/lib/googleauth/id_tokens.rb +11 -25
- data/lib/googleauth/json_key_reader.rb +10 -25
- data/lib/googleauth/oauth2/sts_client.rb +109 -0
- data/lib/googleauth/scope_util.rb +10 -25
- data/lib/googleauth/service_account.rb +11 -28
- data/lib/googleauth/signet.rb +16 -58
- data/lib/googleauth/stores/file_token_store.rb +10 -25
- data/lib/googleauth/stores/redis_token_store.rb +10 -25
- data/lib/googleauth/token_store.rb +10 -25
- data/lib/googleauth/user_authorizer.rb +10 -25
- data/lib/googleauth/user_refresh.rb +15 -27
- data/lib/googleauth/version.rb +11 -26
- data/lib/googleauth/web_user_authorizer.rb +10 -25
- data/lib/googleauth.rb +10 -25
- metadata +26 -11
@@ -1,31 +1,16 @@
|
|
1
|
-
# Copyright 2017
|
2
|
-
# All rights reserved.
|
1
|
+
# Copyright 2017 Google, Inc.
|
3
2
|
#
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
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
|
-
#
|
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
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
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
|
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
|
2
|
-
# All rights reserved.
|
1
|
+
# Copyright 2015 Google, Inc.
|
3
2
|
#
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
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
|
-
#
|
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
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
|
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
|
-
|
49
|
+
CLOUD_SDK_CLIENT_ID = "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.app" \
|
50
|
+
"s.googleusercontent.com".freeze
|
66
51
|
|
67
|
-
CLOUD_SDK_CREDENTIALS_WARNING =
|
68
|
-
"credentials from Google Cloud SDK. We recommend that most
|
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
|
-
"
|
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
|
2
|
-
# All rights reserved.
|
1
|
+
# Copyright 2015 Google, Inc.
|
3
2
|
#
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
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
|
-
#
|
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
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
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
|