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