googleauth 1.3.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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
@@ -0,0 +1,158 @@
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.require "time"
14
+
15
+ require "googleauth/base_client"
16
+ require "googleauth/helpers/connection"
17
+ require "googleauth/oauth2/sts_client"
18
+
19
+ module Google
20
+ # Module Auth provides classes that provide Google-specific authorization
21
+ # used to access Google APIs.
22
+ module Auth
23
+ module ExternalAccount
24
+ # Authenticates requests using External Account credentials, such
25
+ # as those provided by the AWS provider or OIDC provider like Azure, etc.
26
+ module BaseCredentials
27
+ # Contains all methods needed for all external account credentials.
28
+ # Other credentials should call `base_setup` during initialization
29
+ # And should define the :retrieve_subject_token! method
30
+
31
+ # External account JSON type identifier.
32
+ EXTERNAL_ACCOUNT_JSON_TYPE = "external_account".freeze
33
+ # The token exchange grant_type used for exchanging credentials.
34
+ STS_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange".freeze
35
+ # The token exchange requested_token_type. This is always an access_token.
36
+ STS_REQUESTED_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token".freeze
37
+ # Default IAM_SCOPE
38
+ IAM_SCOPE = ["https://www.googleapis.com/auth/iam".freeze].freeze
39
+
40
+ include Google::Auth::BaseClient
41
+ include Helpers::Connection
42
+
43
+ attr_reader :expires_at
44
+ attr_accessor :access_token
45
+
46
+ def expires_within? seconds
47
+ # This method is needed for BaseClient
48
+ @expires_at && @expires_at - Time.now.utc < seconds
49
+ end
50
+
51
+ def expires_at= new_expires_at
52
+ @expires_at = normalize_timestamp new_expires_at
53
+ end
54
+
55
+ def fetch_access_token! _options = {}
56
+ # This method is needed for BaseClient
57
+ response = exchange_token
58
+
59
+ if @service_account_impersonation_url
60
+ impersonated_response = get_impersonated_access_token response["access_token"]
61
+ self.expires_at = impersonated_response["expireTime"]
62
+ self.access_token = impersonated_response["accessToken"]
63
+ else
64
+ # Extract the expiration time in seconds from the response and calculate the actual expiration time
65
+ # and then save that to the expiry variable.
66
+ self.expires_at = Time.now.utc + response["expires_in"].to_i
67
+ self.access_token = response["access_token"]
68
+ end
69
+
70
+ notify_refresh_listeners
71
+ end
72
+
73
+ # Retrieves the subject token using the credential_source object.
74
+ # @return [string]
75
+ # The retrieved subject token.
76
+ #
77
+ def retrieve_subject_token!
78
+ raise NotImplementedError
79
+ end
80
+
81
+ # Returns whether the credentials represent a workforce pool (True) or
82
+ # workload (False) based on the credentials' audience.
83
+ #
84
+ # @return [bool]
85
+ # true if the credentials represent a workforce pool.
86
+ # false if they represent a workload.
87
+ def is_workforce_pool?
88
+ pattern = "//iam\.googleapis\.com/locations/[^/]+/workforcePools/"
89
+ /#{pattern}/.match?(@audience || "")
90
+ end
91
+
92
+ private
93
+
94
+ def token_type
95
+ # This method is needed for BaseClient
96
+ :access_token
97
+ end
98
+
99
+ def base_setup options
100
+ self.default_connection = options[:connection]
101
+
102
+ @audience = options[:audience]
103
+ @scope = options[:scope] || IAM_SCOPE
104
+ @subject_token_type = options[:subject_token_type]
105
+ @token_url = options[:token_url]
106
+ @token_info_url = options[:token_info_url]
107
+ @service_account_impersonation_url = options[:service_account_impersonation_url]
108
+ @service_account_impersonation_options = options[:service_account_impersonation_options] || {}
109
+ @client_id = options[:client_id]
110
+ @client_secret = options[:client_secret]
111
+ @quota_project_id = options[:quota_project_id]
112
+ @project_id = nil
113
+ @workforce_pool_user_project = options[:workforce_pool_user_project]
114
+
115
+ @expires_at = nil
116
+ @access_token = nil
117
+
118
+ @sts_client = Google::Auth::OAuth2::STSClient.new(
119
+ token_exchange_endpoint: @token_url,
120
+ connection: default_connection
121
+ )
122
+ return unless @workforce_pool_user_project && !is_workforce_pool?
123
+ raise "workforce_pool_user_project should not be set for non-workforce pool credentials."
124
+ end
125
+
126
+ def exchange_token
127
+ additional_options = nil
128
+ if @client_id.nil? && @workforce_pool_user_project
129
+ additional_options = { userProject: @workforce_pool_user_project }
130
+ end
131
+ @sts_client.exchange_token(
132
+ audience: @audience,
133
+ grant_type: STS_GRANT_TYPE,
134
+ subject_token: retrieve_subject_token!,
135
+ subject_token_type: @subject_token_type,
136
+ scopes: @service_account_impersonation_url ? IAM_SCOPE : @scope,
137
+ requested_token_type: STS_REQUESTED_TOKEN_TYPE,
138
+ additional_options: additional_options
139
+ )
140
+ end
141
+
142
+ def get_impersonated_access_token token, _options = {}
143
+ response = connection.post @service_account_impersonation_url do |req|
144
+ req.headers["Authorization"] = "Bearer #{token}"
145
+ req.headers["Content-Type"] = "application/json"
146
+ req.body = MultiJson.dump({ scope: @scope })
147
+ end
148
+
149
+ if response.status != 200
150
+ raise "Service account impersonation failed with status #{response.status}"
151
+ end
152
+
153
+ MultiJson.load response.body
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,103 @@
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.require "time"
14
+
15
+ require "googleauth/base_client"
16
+ require "googleauth/helpers/connection"
17
+ require "googleauth/oauth2/sts_client"
18
+
19
+ module Google
20
+ # Module Auth provides classes that provide Google-specific authorization
21
+ # used to access Google APIs.
22
+ module Auth
23
+ module ExternalAccount
24
+ # Authenticates requests using External Account credentials, such
25
+ # as those provided by the AWS provider or OIDC provider like Azure, etc.
26
+ module ExternalAccountUtils
27
+ # Cloud resource manager URL used to retrieve project information.
28
+ CLOUD_RESOURCE_MANAGER = "https://cloudresourcemanager.googleapis.com/v1/projects/".freeze
29
+
30
+ ##
31
+ # Retrieves the project ID corresponding to the workload identity or workforce pool.
32
+ # For workforce pool credentials, it returns the project ID corresponding to the workforce_pool_user_project.
33
+ # When not determinable, None is returned.
34
+ #
35
+ # The resource may not have permission (resourcemanager.projects.get) to
36
+ # call this API or the required scopes may not be selected:
37
+ # https://cloud.google.com/resource-manager/reference/rest/v1/projects/get#authorization-scopes
38
+ #
39
+ # @return [string,nil]
40
+ # The project ID corresponding to the workload identity pool or workforce pool if determinable.
41
+ #
42
+ def project_id
43
+ return @project_id unless @project_id.nil?
44
+ project_number = self.project_number || @workforce_pool_user_project
45
+
46
+ # if we missing either project number or scope, we won't retrieve project_id
47
+ return nil if project_number.nil? || @scope.nil?
48
+
49
+ url = "#{CLOUD_RESOURCE_MANAGER}#{project_number}"
50
+ response = connection.get url do |req|
51
+ req.headers["Authorization"] = "Bearer #{@access_token}"
52
+ req.headers["Content-Type"] = "application/json"
53
+ end
54
+
55
+ if response.status == 200
56
+ response_data = MultiJson.load response.body, symbolize_names: true
57
+ @project_id = response_data[:projectId]
58
+ end
59
+
60
+ @project_id
61
+ end
62
+
63
+ ##
64
+ # Retrieve the project number corresponding to workload identity pool
65
+ # STS audience pattern:
66
+ # `//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/...`
67
+ #
68
+ # @return [string, nil]
69
+ #
70
+ def project_number
71
+ segments = @audience.split "/"
72
+ idx = segments.index "projects"
73
+ return nil if idx.nil? || idx + 1 == segments.size
74
+ segments[idx + 1]
75
+ end
76
+
77
+ def normalize_timestamp time
78
+ case time
79
+ when NilClass
80
+ nil
81
+ when Time
82
+ time
83
+ when String
84
+ Time.parse time
85
+ else
86
+ raise "Invalid time value #{time}"
87
+ end
88
+ end
89
+
90
+ def service_account_email
91
+ return nil if @service_account_impersonation_url.nil?
92
+ start_idx = @service_account_impersonation_url.rindex "/"
93
+ end_idx = @service_account_impersonation_url.index ":generateAccessToken"
94
+ if start_idx != -1 && end_idx != -1 && start_idx < end_idx
95
+ start_idx += 1
96
+ return @service_account_impersonation_url[start_idx..end_idx]
97
+ end
98
+ nil
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end