googleauth 1.14.0 → 1.17.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 +49 -0
- data/Credentials.md +110 -0
- data/Errors.md +152 -0
- data/README.md +0 -1
- data/lib/googleauth/api_key.rb +9 -0
- data/lib/googleauth/application_default.rb +3 -1
- data/lib/googleauth/base_client.rb +5 -0
- data/lib/googleauth/bearer_token.rb +16 -2
- data/lib/googleauth/client_id.rb +15 -7
- data/lib/googleauth/compute_engine.rb +64 -18
- data/lib/googleauth/credentials.rb +66 -35
- data/lib/googleauth/credentials_loader.rb +30 -5
- data/lib/googleauth/default_credentials.rb +69 -34
- data/lib/googleauth/errors.rb +117 -0
- data/lib/googleauth/external_account/aws_credentials.rb +87 -19
- data/lib/googleauth/external_account/base_credentials.rb +34 -4
- data/lib/googleauth/external_account/external_account_utils.rb +17 -5
- data/lib/googleauth/external_account/identity_pool_credentials.rb +42 -16
- data/lib/googleauth/external_account/pluggable_credentials.rb +37 -21
- data/lib/googleauth/external_account.rb +53 -7
- data/lib/googleauth/iam.rb +20 -4
- data/lib/googleauth/id_tokens/errors.rb +13 -7
- data/lib/googleauth/id_tokens/key_sources.rb +13 -7
- data/lib/googleauth/id_tokens/verifier.rb +2 -3
- data/lib/googleauth/id_tokens.rb +4 -4
- data/lib/googleauth/impersonated_service_account.rb +119 -19
- data/lib/googleauth/json_key_reader.rb +17 -3
- data/lib/googleauth/oauth2/sts_client.rb +12 -6
- data/lib/googleauth/scope_util.rb +2 -2
- data/lib/googleauth/service_account.rb +55 -21
- data/lib/googleauth/service_account_jwt_header.rb +9 -2
- data/lib/googleauth/signet.rb +30 -10
- data/lib/googleauth/user_authorizer.rb +38 -10
- data/lib/googleauth/user_refresh.rb +54 -19
- data/lib/googleauth/version.rb +1 -1
- data/lib/googleauth/web_user_authorizer.rb +54 -17
- data/lib/googleauth.rb +1 -0
- metadata +22 -19
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
15
|
require "time"
|
|
16
|
+
require "json"
|
|
17
|
+
require "googleauth/errors"
|
|
16
18
|
require "googleauth/external_account/base_credentials"
|
|
17
19
|
require "googleauth/external_account/external_account_utils"
|
|
18
20
|
|
|
@@ -106,17 +108,32 @@ module Google
|
|
|
106
108
|
|
|
107
109
|
private
|
|
108
110
|
|
|
111
|
+
# Retrieves an IMDSv2 session token or returns a cached token if valid
|
|
112
|
+
#
|
|
113
|
+
# @return [String] The IMDSv2 session token
|
|
114
|
+
# @raise [Google::Auth::CredentialsError] If the token URL is missing or there's an error retrieving the token
|
|
109
115
|
def imdsv2_session_token
|
|
110
116
|
return @imdsv2_session_token unless imdsv2_session_token_invalid?
|
|
111
|
-
|
|
117
|
+
if @imdsv2_session_token_url.nil?
|
|
118
|
+
raise CredentialsError.with_details(
|
|
119
|
+
"IMDSV2 token url must be provided",
|
|
120
|
+
credential_type_name: self.class.name,
|
|
121
|
+
principal: principal
|
|
122
|
+
)
|
|
123
|
+
end
|
|
112
124
|
begin
|
|
113
125
|
response = connection.put @imdsv2_session_token_url do |req|
|
|
114
126
|
req.headers["x-aws-ec2-metadata-token-ttl-seconds"] = IMDSV2_TOKEN_EXPIRATION_IN_SECONDS.to_s
|
|
115
127
|
end
|
|
128
|
+
raise Faraday::Error unless response.success?
|
|
116
129
|
rescue Faraday::Error => e
|
|
117
|
-
raise
|
|
130
|
+
raise CredentialsError.with_details(
|
|
131
|
+
"Fetching AWS IMDSV2 token error: #{e}",
|
|
132
|
+
credential_type_name: self.class.name,
|
|
133
|
+
principal: principal
|
|
134
|
+
)
|
|
118
135
|
end
|
|
119
|
-
|
|
136
|
+
|
|
120
137
|
@imdsv2_session_token = response.body
|
|
121
138
|
@imdsv2_session_token_expiry = Time.now + IMDSV2_TOKEN_EXPIRATION_IN_SECONDS
|
|
122
139
|
@imdsv2_session_token
|
|
@@ -127,6 +144,14 @@ module Google
|
|
|
127
144
|
@imdsv2_session_token_expiry.nil? || @imdsv2_session_token_expiry < Time.now
|
|
128
145
|
end
|
|
129
146
|
|
|
147
|
+
# Makes a request to an AWS resource endpoint
|
|
148
|
+
#
|
|
149
|
+
# @param [String] url The AWS endpoint URL
|
|
150
|
+
# @param [String] name Resource name for error messages
|
|
151
|
+
# @param [Hash, nil] data Optional data to send in POST requests
|
|
152
|
+
# @param [Hash] headers Optional request headers
|
|
153
|
+
# @return [Faraday::Response] The successful response
|
|
154
|
+
# @raise [Google::Auth::CredentialsError] If the request fails
|
|
130
155
|
def get_aws_resource url, name, data: nil, headers: {}
|
|
131
156
|
begin
|
|
132
157
|
headers["x-aws-ec2-metadata-token"] = imdsv2_session_token
|
|
@@ -136,11 +161,14 @@ module Google
|
|
|
136
161
|
else
|
|
137
162
|
connection.get url, nil, headers
|
|
138
163
|
end
|
|
139
|
-
|
|
140
164
|
raise Faraday::Error unless response.success?
|
|
141
165
|
response
|
|
142
166
|
rescue Faraday::Error
|
|
143
|
-
raise
|
|
167
|
+
raise CredentialsError.with_details(
|
|
168
|
+
"Failed to retrieve AWS #{name}.",
|
|
169
|
+
credential_type_name: self.class.name,
|
|
170
|
+
principal: principal
|
|
171
|
+
)
|
|
144
172
|
end
|
|
145
173
|
end
|
|
146
174
|
|
|
@@ -181,9 +209,16 @@ module Google
|
|
|
181
209
|
# Retrieves the AWS role currently attached to the current AWS workload by querying the AWS metadata server.
|
|
182
210
|
# This is needed for the AWS metadata server security credentials endpoint in order to retrieve the AWS security
|
|
183
211
|
# credentials needed to sign requests to AWS APIs.
|
|
212
|
+
#
|
|
213
|
+
# @return [String] The AWS role name
|
|
214
|
+
# @raise [Google::Auth::CredentialsError] If the credential verification URL is not set or if the request fails
|
|
184
215
|
def fetch_metadata_role_name
|
|
185
216
|
unless @credential_verification_url
|
|
186
|
-
raise
|
|
217
|
+
raise CredentialsError.with_details(
|
|
218
|
+
"Unable to determine the AWS metadata server security credentials endpoint",
|
|
219
|
+
credential_type_name: self.class.name,
|
|
220
|
+
principal: principal
|
|
221
|
+
)
|
|
187
222
|
end
|
|
188
223
|
|
|
189
224
|
get_aws_resource(@credential_verification_url, "IAM Role").body
|
|
@@ -192,14 +227,25 @@ module Google
|
|
|
192
227
|
# Retrieves the AWS security credentials required for signing AWS requests from the AWS metadata server.
|
|
193
228
|
def fetch_metadata_security_credentials role_name
|
|
194
229
|
response = get_aws_resource "#{@credential_verification_url}/#{role_name}", "credentials"
|
|
195
|
-
|
|
230
|
+
JSON.parse response.body
|
|
196
231
|
end
|
|
197
232
|
|
|
233
|
+
# Reads the name of the AWS region from the environment
|
|
234
|
+
#
|
|
235
|
+
# @return [String] The name of the AWS region
|
|
236
|
+
# @raise [Google::Auth::CredentialsError] If the region is not set in the environment
|
|
237
|
+
# and the region_url was not set in credentials source
|
|
198
238
|
def region
|
|
199
239
|
@region = ENV[CredentialsLoader::AWS_REGION_VAR] || ENV[CredentialsLoader::AWS_DEFAULT_REGION_VAR]
|
|
200
240
|
|
|
201
241
|
unless @region
|
|
202
|
-
|
|
242
|
+
unless @region_url
|
|
243
|
+
raise CredentialsError.with_details(
|
|
244
|
+
"region_url or region must be set for external account credentials",
|
|
245
|
+
credential_type_name: self.class.name,
|
|
246
|
+
principal: principal
|
|
247
|
+
)
|
|
248
|
+
end
|
|
203
249
|
|
|
204
250
|
@region ||= get_aws_resource(@region_url, "region").body[0..-2]
|
|
205
251
|
end
|
|
@@ -220,23 +266,45 @@ module Google
|
|
|
220
266
|
@region_name = region_name
|
|
221
267
|
end
|
|
222
268
|
|
|
223
|
-
# Generates
|
|
224
|
-
#
|
|
269
|
+
# Generates an AWS signature version 4 signed request.
|
|
270
|
+
#
|
|
271
|
+
# Creates a signed request following the AWS Signature Version 4 process, which
|
|
272
|
+
# provides secure authentication for AWS API calls. The process includes creating
|
|
273
|
+
# canonical request strings, calculating signatures using the AWS credentials, and
|
|
274
|
+
# building proper authorization headers.
|
|
275
|
+
#
|
|
276
|
+
# For detailed information on the signing process, see:
|
|
225
277
|
# https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
|
|
226
278
|
#
|
|
227
|
-
# @param [Hash
|
|
228
|
-
#
|
|
229
|
-
#
|
|
230
|
-
#
|
|
231
|
-
# @param [
|
|
232
|
-
#
|
|
279
|
+
# @param [Hash] aws_credentials The AWS security credentials with the following keys:
|
|
280
|
+
# @option aws_credentials [String] :access_key_id The AWS access key ID
|
|
281
|
+
# @option aws_credentials [String] :secret_access_key The AWS secret access key
|
|
282
|
+
# @option aws_credentials [String, nil] :session_token Optional AWS session token
|
|
283
|
+
# @param [Hash] original_request The request to sign with the following keys:
|
|
284
|
+
# @option original_request [String] :url The AWS service URL (must be HTTPS)
|
|
285
|
+
# @option original_request [String] :method The HTTP method (GET, POST, etc.)
|
|
286
|
+
# @option original_request [Hash, nil] :headers Optional request headers
|
|
287
|
+
# @option original_request [String, nil] :data Optional request payload
|
|
288
|
+
#
|
|
289
|
+
# @return [Hash] The signed request with the following keys:
|
|
290
|
+
# * :url - The original URL as a string
|
|
291
|
+
# * :headers - A hash of headers with the authorization header added
|
|
292
|
+
# * :method - The HTTP method
|
|
293
|
+
# * :data - The request payload (if present)
|
|
233
294
|
#
|
|
234
|
-
# @
|
|
235
|
-
# The AWS signed request dictionary object.
|
|
295
|
+
# @raise [Google::Auth::CredentialsError] If the AWS service URL is invalid
|
|
236
296
|
#
|
|
237
297
|
def generate_signed_request aws_credentials, original_request
|
|
238
298
|
uri = Addressable::URI.parse original_request[:url]
|
|
239
|
-
|
|
299
|
+
unless uri.hostname && uri.scheme == "https"
|
|
300
|
+
# NOTE: We use AwsCredentials name but can't access its principal since AwsRequestSigner
|
|
301
|
+
# is a separate class and not a credential object with access to the audience
|
|
302
|
+
raise CredentialsError.with_details(
|
|
303
|
+
"Invalid AWS service URL",
|
|
304
|
+
credential_type_name: AwsCredentials.name,
|
|
305
|
+
principal: "aws"
|
|
306
|
+
)
|
|
307
|
+
end
|
|
240
308
|
service_name = uri.host.split(".").first
|
|
241
309
|
|
|
242
310
|
datetime = Time.now.utc.strftime "%Y%m%dT%H%M%SZ"
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.require "time"
|
|
14
14
|
|
|
15
|
+
require "json"
|
|
15
16
|
require "googleauth/base_client"
|
|
17
|
+
require "googleauth/errors"
|
|
16
18
|
require "googleauth/helpers/connection"
|
|
17
19
|
require "googleauth/oauth2/sts_client"
|
|
18
20
|
|
|
@@ -89,6 +91,14 @@ module Google
|
|
|
89
91
|
%r{/iam\.googleapis\.com/locations/[^/]+/workforcePools/}.match?(@audience || "")
|
|
90
92
|
end
|
|
91
93
|
|
|
94
|
+
# For external account credentials, the principal is
|
|
95
|
+
# represented by the audience, such as a workforce pool
|
|
96
|
+
# @private
|
|
97
|
+
# @return [String] the GCP principal, e.g. a workforce pool
|
|
98
|
+
def principal
|
|
99
|
+
@audience
|
|
100
|
+
end
|
|
101
|
+
|
|
92
102
|
private
|
|
93
103
|
|
|
94
104
|
def token_type
|
|
@@ -96,6 +106,8 @@ module Google
|
|
|
96
106
|
:access_token
|
|
97
107
|
end
|
|
98
108
|
|
|
109
|
+
# A common method for Other credentials to call during initialization
|
|
110
|
+
# @raise [Google::Auth::InitializationError] If workforce_pool_user_project is incorrectly set
|
|
99
111
|
def base_setup options
|
|
100
112
|
self.default_connection = options[:connection]
|
|
101
113
|
|
|
@@ -121,9 +133,11 @@ module Google
|
|
|
121
133
|
connection: default_connection
|
|
122
134
|
)
|
|
123
135
|
return unless @workforce_pool_user_project && !is_workforce_pool?
|
|
124
|
-
raise "workforce_pool_user_project should not be set for non-workforce pool credentials."
|
|
136
|
+
raise InitializationError, "workforce_pool_user_project should not be set for non-workforce pool credentials."
|
|
125
137
|
end
|
|
126
138
|
|
|
139
|
+
# Exchange tokens at STS endpoint
|
|
140
|
+
# @raise [Google::Auth::AuthorizationError] If the token exchange request fails
|
|
127
141
|
def exchange_token
|
|
128
142
|
additional_options = nil
|
|
129
143
|
if @client_id.nil? && @workforce_pool_user_project
|
|
@@ -140,6 +154,12 @@ module Google
|
|
|
140
154
|
}
|
|
141
155
|
log_token_request token_request
|
|
142
156
|
@sts_client.exchange_token token_request
|
|
157
|
+
rescue Google::Auth::AuthorizationError => e
|
|
158
|
+
raise Google::Auth::AuthorizationError.with_details(
|
|
159
|
+
e.message,
|
|
160
|
+
credential_type_name: self.class.name,
|
|
161
|
+
principal: principal
|
|
162
|
+
)
|
|
143
163
|
end
|
|
144
164
|
|
|
145
165
|
def log_token_request token_request
|
|
@@ -160,19 +180,29 @@ module Google
|
|
|
160
180
|
end
|
|
161
181
|
end
|
|
162
182
|
|
|
183
|
+
# Exchanges a token for an impersonated service account access token
|
|
184
|
+
#
|
|
185
|
+
# @param [String] token The token to exchange
|
|
186
|
+
# @param [Hash] _options Additional options (not used)
|
|
187
|
+
# @return [Hash] The response containing the impersonated access token
|
|
188
|
+
# @raise [Google::Auth::CredentialsError] If the impersonation request fails
|
|
163
189
|
def get_impersonated_access_token token, _options = {}
|
|
164
190
|
log_impersonated_token_request token
|
|
165
191
|
response = connection.post @service_account_impersonation_url do |req|
|
|
166
192
|
req.headers["Authorization"] = "Bearer #{token}"
|
|
167
193
|
req.headers["Content-Type"] = "application/json"
|
|
168
|
-
req.body =
|
|
194
|
+
req.body = JSON.generate({ scope: @scope })
|
|
169
195
|
end
|
|
170
196
|
|
|
171
197
|
if response.status != 200
|
|
172
|
-
raise
|
|
198
|
+
raise CredentialsError.with_details(
|
|
199
|
+
"Service account impersonation failed with status #{response.status}",
|
|
200
|
+
credential_type_name: self.class.name,
|
|
201
|
+
principal: principal
|
|
202
|
+
)
|
|
173
203
|
end
|
|
174
204
|
|
|
175
|
-
|
|
205
|
+
JSON.parse response.body
|
|
176
206
|
end
|
|
177
207
|
|
|
178
208
|
def log_impersonated_token_request original_token
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.require "time"
|
|
14
14
|
|
|
15
|
+
require "json"
|
|
15
16
|
require "googleauth/base_client"
|
|
17
|
+
require "googleauth/errors"
|
|
16
18
|
require "googleauth/helpers/connection"
|
|
17
19
|
require "googleauth/oauth2/sts_client"
|
|
18
20
|
|
|
@@ -36,8 +38,8 @@ module Google
|
|
|
36
38
|
# call this API or the required scopes may not be selected:
|
|
37
39
|
# https://cloud.google.com/resource-manager/reference/rest/v1/projects/get#authorization-scopes
|
|
38
40
|
#
|
|
39
|
-
# @return [
|
|
40
|
-
#
|
|
41
|
+
# @return [String, nil] The project ID corresponding to the workload identity
|
|
42
|
+
# pool or workforce pool if determinable
|
|
41
43
|
#
|
|
42
44
|
def project_id
|
|
43
45
|
return @project_id unless @project_id.nil?
|
|
@@ -53,7 +55,7 @@ module Google
|
|
|
53
55
|
end
|
|
54
56
|
|
|
55
57
|
if response.status == 200
|
|
56
|
-
response_data =
|
|
58
|
+
response_data = JSON.parse response.body, symbolize_names: true
|
|
57
59
|
@project_id = response_data[:projectId]
|
|
58
60
|
end
|
|
59
61
|
|
|
@@ -65,7 +67,8 @@ module Google
|
|
|
65
67
|
# STS audience pattern:
|
|
66
68
|
# `//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/...`
|
|
67
69
|
#
|
|
68
|
-
# @return [
|
|
70
|
+
# @return [String, nil] The project number extracted from the audience string,
|
|
71
|
+
# or nil if it cannot be determined
|
|
69
72
|
#
|
|
70
73
|
def project_number
|
|
71
74
|
segments = @audience.split "/"
|
|
@@ -74,6 +77,11 @@ module Google
|
|
|
74
77
|
segments[idx + 1]
|
|
75
78
|
end
|
|
76
79
|
|
|
80
|
+
# Normalizes a timestamp value to a Time object
|
|
81
|
+
#
|
|
82
|
+
# @param time [Time, String, nil] The timestamp to normalize
|
|
83
|
+
# @return [Time, nil] The normalized timestamp or nil if input is nil
|
|
84
|
+
# @raise [Google::Auth::CredentialsError] If the time value is not nil, Time, or String
|
|
77
85
|
def normalize_timestamp time
|
|
78
86
|
case time
|
|
79
87
|
when NilClass
|
|
@@ -83,10 +91,14 @@ module Google
|
|
|
83
91
|
when String
|
|
84
92
|
Time.parse time
|
|
85
93
|
else
|
|
86
|
-
raise "Invalid time value #{time}"
|
|
94
|
+
raise CredentialsError, "Invalid time value #{time}"
|
|
87
95
|
end
|
|
88
96
|
end
|
|
89
97
|
|
|
98
|
+
# Extracts the service account email from the impersonation URL
|
|
99
|
+
#
|
|
100
|
+
# @return [String, nil] The service account email extracted from the
|
|
101
|
+
# service_account_impersonation_url, or nil if it cannot be determined
|
|
90
102
|
def service_account_email
|
|
91
103
|
return nil if @service_account_impersonation_url.nil?
|
|
92
104
|
start_idx = @service_account_impersonation_url.rindex "/"
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
15
|
require "time"
|
|
16
|
+
require "json"
|
|
17
|
+
require "googleauth/errors"
|
|
16
18
|
require "googleauth/external_account/base_credentials"
|
|
17
19
|
require "googleauth/external_account/external_account_utils"
|
|
18
20
|
|
|
@@ -32,10 +34,12 @@ module Google
|
|
|
32
34
|
|
|
33
35
|
# Initialize from options map.
|
|
34
36
|
#
|
|
35
|
-
# @param [
|
|
36
|
-
# @
|
|
37
|
-
#
|
|
38
|
-
# credential_source_format is either text or json
|
|
37
|
+
# @param [Hash] options Configuration options
|
|
38
|
+
# @option options [String] :audience The audience for the token
|
|
39
|
+
# @option options [Hash{Symbol => Object}] :credential_source A hash containing either source file or url.
|
|
40
|
+
# credential_source_format is either text or json to define how to parse the credential response.
|
|
41
|
+
# @raise [Google::Auth::InitializationError] If credential_source format is invalid, field_name is missing,
|
|
42
|
+
# contains ambiguous sources, or is missing required fields
|
|
39
43
|
#
|
|
40
44
|
def initialize options = {}
|
|
41
45
|
base_setup options
|
|
@@ -51,65 +55,87 @@ module Google
|
|
|
51
55
|
end
|
|
52
56
|
|
|
53
57
|
# Implementation of BaseCredentials retrieve_subject_token!
|
|
58
|
+
#
|
|
59
|
+
# @return [String] The subject token
|
|
60
|
+
# @raise [Google::Auth::CredentialsError] If the token can't be parsed from JSON or is missing
|
|
54
61
|
def retrieve_subject_token!
|
|
55
62
|
content, resource_name = token_data
|
|
56
63
|
if @credential_source_format_type == "text"
|
|
57
64
|
token = content
|
|
58
65
|
else
|
|
59
66
|
begin
|
|
60
|
-
response_data =
|
|
67
|
+
response_data = JSON.parse content, symbolize_names: true
|
|
61
68
|
token = response_data[@credential_source_field_name.to_sym]
|
|
62
69
|
rescue StandardError
|
|
63
|
-
raise "Unable to parse subject_token from JSON resource #{resource_name} " \
|
|
64
|
-
|
|
70
|
+
raise CredentialsError, "Unable to parse subject_token from JSON resource #{resource_name} " \
|
|
71
|
+
"using key #{@credential_source_field_name}"
|
|
65
72
|
end
|
|
66
73
|
end
|
|
67
|
-
raise "Missing subject_token in the credential_source file/response." unless token
|
|
74
|
+
raise CredentialsError, "Missing subject_token in the credential_source file/response." unless token
|
|
68
75
|
token
|
|
69
76
|
end
|
|
70
77
|
|
|
71
78
|
private
|
|
72
79
|
|
|
80
|
+
# Validates input
|
|
81
|
+
#
|
|
82
|
+
# @raise [Google::Auth::InitializationError] If credential_source format is invalid, field_name is missing,
|
|
83
|
+
# contains ambiguous sources, or is missing required fields
|
|
73
84
|
def validate_credential_source
|
|
74
85
|
# `environment_id` is only supported in AWS or dedicated future external account credentials.
|
|
75
86
|
unless @credential_source[:environment_id].nil?
|
|
76
|
-
raise "Invalid Identity Pool credential_source field 'environment_id'"
|
|
87
|
+
raise InitializationError, "Invalid Identity Pool credential_source field 'environment_id'"
|
|
77
88
|
end
|
|
78
89
|
unless ["json", "text"].include? @credential_source_format_type
|
|
79
|
-
raise "Invalid credential_source format #{@credential_source_format_type}"
|
|
90
|
+
raise InitializationError, "Invalid credential_source format #{@credential_source_format_type}"
|
|
80
91
|
end
|
|
81
92
|
# for JSON types, get the required subject_token field name.
|
|
82
93
|
@credential_source_field_name = @credential_source_format[:subject_token_field_name]
|
|
83
94
|
if @credential_source_format_type == "json" && @credential_source_field_name.nil?
|
|
84
|
-
raise "Missing subject_token_field_name for JSON credential_source format"
|
|
95
|
+
raise InitializationError, "Missing subject_token_field_name for JSON credential_source format"
|
|
85
96
|
end
|
|
86
97
|
# check file or url must be fulfilled and mutually exclusiveness.
|
|
87
98
|
if @credential_source_file && @credential_source_url
|
|
88
|
-
raise "Ambiguous credential_source. 'file' is mutually exclusive with 'url'."
|
|
99
|
+
raise InitializationError, "Ambiguous credential_source. 'file' is mutually exclusive with 'url'."
|
|
89
100
|
end
|
|
90
101
|
return unless (@credential_source_file || @credential_source_url).nil?
|
|
91
|
-
raise "Missing credential_source. A 'file' or 'url' must be provided."
|
|
102
|
+
raise InitializationError, "Missing credential_source. A 'file' or 'url' must be provided."
|
|
92
103
|
end
|
|
93
104
|
|
|
94
105
|
def token_data
|
|
95
106
|
@credential_source_file.nil? ? url_data : file_data
|
|
96
107
|
end
|
|
97
108
|
|
|
109
|
+
# Reads data from a file source
|
|
110
|
+
#
|
|
111
|
+
# @return [Array(String, String)] The file content and file path
|
|
112
|
+
# @raise [Google::Auth::CredentialsError] If the source file doesn't exist
|
|
98
113
|
def file_data
|
|
99
|
-
|
|
114
|
+
unless File.exist? @credential_source_file
|
|
115
|
+
raise CredentialsError,
|
|
116
|
+
"File #{@credential_source_file} was not found."
|
|
117
|
+
end
|
|
100
118
|
content = File.read @credential_source_file, encoding: "utf-8"
|
|
101
119
|
[content, @credential_source_file]
|
|
102
120
|
end
|
|
103
121
|
|
|
122
|
+
# Fetches data from a URL source
|
|
123
|
+
#
|
|
124
|
+
# @return [Array(String, String)] The response body and URL
|
|
125
|
+
# @raise [Google::Auth::CredentialsError] If there's an error retrieving data from the URL
|
|
126
|
+
# or if the response is not successful
|
|
104
127
|
def url_data
|
|
105
128
|
begin
|
|
106
129
|
response = connection.get @credential_source_url do |req|
|
|
107
130
|
req.headers.merge! @credential_source_headers
|
|
108
131
|
end
|
|
109
132
|
rescue Faraday::Error => e
|
|
110
|
-
raise "Error retrieving from credential url: #{e}"
|
|
133
|
+
raise CredentialsError, "Error retrieving from credential url: #{e}"
|
|
134
|
+
end
|
|
135
|
+
unless response.success?
|
|
136
|
+
raise CredentialsError,
|
|
137
|
+
"Unable to retrieve Identity Pool subject token #{response.body}"
|
|
111
138
|
end
|
|
112
|
-
raise "Unable to retrieve Identity Pool subject token #{response.body}" unless response.success?
|
|
113
139
|
[response.body, @credential_source_url]
|
|
114
140
|
end
|
|
115
141
|
end
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
require "open3"
|
|
16
16
|
require "time"
|
|
17
|
+
require "json"
|
|
18
|
+
require "googleauth/errors"
|
|
17
19
|
require "googleauth/external_account/base_credentials"
|
|
18
20
|
require "googleauth/external_account/external_account_utils"
|
|
19
21
|
|
|
@@ -41,34 +43,43 @@ module Google
|
|
|
41
43
|
|
|
42
44
|
# Initialize from options map.
|
|
43
45
|
#
|
|
44
|
-
# @param [
|
|
45
|
-
# @
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
46
|
+
# @param [Hash] options Configuration options
|
|
47
|
+
# @option options [String] :audience Audience for the token
|
|
48
|
+
# @option options [Hash] :credential_source Credential source configuration that contains executable
|
|
49
|
+
# configuration
|
|
50
|
+
# @raise [Google::Auth::InitializationError] If executable source, command is missing, or timeout is invalid
|
|
49
51
|
def initialize options = {}
|
|
50
52
|
base_setup options
|
|
51
53
|
|
|
52
54
|
@audience = options[:audience]
|
|
53
55
|
@credential_source = options[:credential_source] || {}
|
|
54
56
|
@credential_source_executable = @credential_source[:executable]
|
|
55
|
-
|
|
57
|
+
if @credential_source_executable.nil?
|
|
58
|
+
raise InitializationError,
|
|
59
|
+
"Missing excutable source. An 'executable' must be provided"
|
|
60
|
+
end
|
|
56
61
|
@credential_source_executable_command = @credential_source_executable[:command]
|
|
57
62
|
if @credential_source_executable_command.nil?
|
|
58
|
-
raise "Missing command field. Executable command must be provided."
|
|
63
|
+
raise InitializationError, "Missing command field. Executable command must be provided."
|
|
59
64
|
end
|
|
60
65
|
@credential_source_executable_timeout_millis = @credential_source_executable[:timeout_millis] ||
|
|
61
66
|
EXECUTABLE_TIMEOUT_MILLIS_DEFAULT
|
|
62
67
|
if @credential_source_executable_timeout_millis < EXECUTABLE_TIMEOUT_MILLIS_LOWER_BOUND ||
|
|
63
68
|
@credential_source_executable_timeout_millis > EXECUTABLE_TIMEOUT_MILLIS_UPPER_BOUND
|
|
64
|
-
raise "Timeout must be between 5 and 120 seconds."
|
|
69
|
+
raise InitializationError, "Timeout must be between 5 and 120 seconds."
|
|
65
70
|
end
|
|
66
71
|
@credential_source_executable_output_file = @credential_source_executable[:output_file]
|
|
67
72
|
end
|
|
68
73
|
|
|
74
|
+
# Retrieves the subject token using the credential_source object.
|
|
75
|
+
#
|
|
76
|
+
# @return [String] The retrieved subject token
|
|
77
|
+
# @raise [Google::Auth::CredentialsError] If executables are not allowed, if token retrieval fails,
|
|
78
|
+
# or if the token is invalid
|
|
69
79
|
def retrieve_subject_token!
|
|
70
80
|
unless ENV[ENABLE_PLUGGABLE_ENV] == "1"
|
|
71
|
-
raise
|
|
81
|
+
raise CredentialsError,
|
|
82
|
+
"Executables need to be explicitly allowed (set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES to '1') " \
|
|
72
83
|
"to run."
|
|
73
84
|
end
|
|
74
85
|
# check output file first
|
|
@@ -78,7 +89,7 @@ module Google
|
|
|
78
89
|
env = inject_environment_variables
|
|
79
90
|
output = subprocess_with_timeout env, @credential_source_executable_command,
|
|
80
91
|
@credential_source_executable_timeout_millis
|
|
81
|
-
response =
|
|
92
|
+
response = JSON.parse output, symbolize_names: true
|
|
82
93
|
parse_subject_token response
|
|
83
94
|
end
|
|
84
95
|
|
|
@@ -89,7 +100,7 @@ module Google
|
|
|
89
100
|
return nil unless File.exist? @credential_source_executable_output_file
|
|
90
101
|
begin
|
|
91
102
|
content = File.read @credential_source_executable_output_file, encoding: "utf-8"
|
|
92
|
-
response =
|
|
103
|
+
response = JSON.parse content, symbolize_names: true
|
|
93
104
|
rescue StandardError
|
|
94
105
|
return nil
|
|
95
106
|
end
|
|
@@ -97,7 +108,7 @@ module Google
|
|
|
97
108
|
subject_token = parse_subject_token response
|
|
98
109
|
rescue StandardError => e
|
|
99
110
|
return nil if e.message.match(/The token returned by the executable is expired/)
|
|
100
|
-
raise e
|
|
111
|
+
raise CredentialsError, e.message
|
|
101
112
|
end
|
|
102
113
|
subject_token
|
|
103
114
|
end
|
|
@@ -106,25 +117,29 @@ module Google
|
|
|
106
117
|
validate_response_schema response
|
|
107
118
|
unless response[:success]
|
|
108
119
|
if response[:code].nil? || response[:message].nil?
|
|
109
|
-
raise "Error code and message fields are required in the response."
|
|
120
|
+
raise CredentialsError, "Error code and message fields are required in the response."
|
|
110
121
|
end
|
|
111
|
-
raise
|
|
122
|
+
raise CredentialsError,
|
|
123
|
+
"Executable returned unsuccessful response: code: #{response[:code]}, message: #{response[:message]}."
|
|
112
124
|
end
|
|
113
125
|
if response[:expiration_time] && response[:expiration_time] < Time.now.to_i
|
|
114
|
-
raise "The token returned by the executable is expired."
|
|
126
|
+
raise CredentialsError, "The token returned by the executable is expired."
|
|
127
|
+
end
|
|
128
|
+
if response[:token_type].nil?
|
|
129
|
+
raise CredentialsError,
|
|
130
|
+
"The executable response is missing the token_type field."
|
|
115
131
|
end
|
|
116
|
-
raise "The executable response is missing the token_type field." if response[:token_type].nil?
|
|
117
132
|
return response[:id_token] if ID_TOKEN_TYPE.include? response[:token_type]
|
|
118
133
|
return response[:saml_response] if response[:token_type] == "urn:ietf:params:oauth:token-type:saml2"
|
|
119
|
-
raise "Executable returned unsupported token type."
|
|
134
|
+
raise CredentialsError, "Executable returned unsupported token type."
|
|
120
135
|
end
|
|
121
136
|
|
|
122
137
|
def validate_response_schema response
|
|
123
|
-
raise "The executable response is missing the version field." if response[:version].nil?
|
|
138
|
+
raise CredentialsError, "The executable response is missing the version field." if response[:version].nil?
|
|
124
139
|
if response[:version] > EXECUTABLE_SUPPORTED_MAX_VERSION
|
|
125
|
-
raise "Executable returned unsupported version #{response[:version]}."
|
|
140
|
+
raise CredentialsError, "Executable returned unsupported version #{response[:version]}."
|
|
126
141
|
end
|
|
127
|
-
raise "The executable response is missing the success field." if response[:success].nil?
|
|
142
|
+
raise CredentialsError, "The executable response is missing the success field." if response[:success].nil?
|
|
128
143
|
end
|
|
129
144
|
|
|
130
145
|
def inject_environment_variables
|
|
@@ -145,7 +160,8 @@ module Google
|
|
|
145
160
|
Timeout.timeout timeout_seconds do
|
|
146
161
|
output, error, status = Open3.capture3 environment_vars, command
|
|
147
162
|
unless status.success?
|
|
148
|
-
raise
|
|
163
|
+
raise CredentialsError,
|
|
164
|
+
"Executable exited with non-zero return code #{status.exitstatus}. Error: #{output}, #{error}"
|
|
149
165
|
end
|
|
150
166
|
output
|
|
151
167
|
end
|