googleauth 1.8.0 → 1.15.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 +117 -0
- data/Credentials.md +106 -0
- data/Errors.md +152 -0
- data/README.md +49 -1
- data/lib/googleauth/api_key.rb +164 -0
- data/lib/googleauth/application_default.rb +6 -8
- data/lib/googleauth/base_client.rb +21 -4
- data/lib/googleauth/bearer_token.rb +162 -0
- data/lib/googleauth/client_id.rb +9 -6
- data/lib/googleauth/compute_engine.rb +231 -49
- data/lib/googleauth/credentials.rb +187 -58
- data/lib/googleauth/credentials_loader.rb +11 -20
- data/lib/googleauth/default_credentials.rb +29 -8
- data/lib/googleauth/errors.rb +117 -0
- data/lib/googleauth/external_account/aws_credentials.rb +85 -18
- data/lib/googleauth/external_account/base_credentials.rb +67 -6
- data/lib/googleauth/external_account/external_account_utils.rb +15 -4
- data/lib/googleauth/external_account/identity_pool_credentials.rb +40 -15
- data/lib/googleauth/external_account/pluggable_credentials.rb +34 -19
- data/lib/googleauth/external_account.rb +32 -7
- data/lib/googleauth/helpers/connection.rb +7 -1
- data/lib/googleauth/iam.rb +19 -3
- 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 -6
- data/lib/googleauth/impersonated_service_account.rb +329 -0
- data/lib/googleauth/json_key_reader.rb +13 -3
- data/lib/googleauth/oauth2/sts_client.rb +9 -4
- data/lib/googleauth/scope_util.rb +1 -1
- data/lib/googleauth/service_account.rb +84 -104
- data/lib/googleauth/service_account_jwt_header.rb +187 -0
- data/lib/googleauth/signet.rb +169 -4
- data/lib/googleauth/token_store.rb +3 -3
- data/lib/googleauth/user_authorizer.rb +89 -11
- data/lib/googleauth/user_refresh.rb +72 -9
- data/lib/googleauth/version.rb +1 -1
- data/lib/googleauth/web_user_authorizer.rb +65 -17
- data/lib/googleauth.rb +8 -0
- metadata +45 -13
@@ -15,6 +15,7 @@
|
|
15
15
|
require "time"
|
16
16
|
require "uri"
|
17
17
|
require "googleauth/credentials_loader"
|
18
|
+
require "googleauth/errors"
|
18
19
|
require "googleauth/external_account/aws_credentials"
|
19
20
|
require "googleauth/external_account/identity_pool_credentials"
|
20
21
|
require "googleauth/external_account/pluggable_credentials"
|
@@ -35,30 +36,41 @@ module Google
|
|
35
36
|
|
36
37
|
# Create a ExternalAccount::Credentials
|
37
38
|
#
|
38
|
-
# @param
|
39
|
-
# @
|
39
|
+
# @param options [Hash] Options for creating credentials
|
40
|
+
# @option options [IO] :json_key_io (required) An IO object containing the JSON key
|
41
|
+
# @option options [String,Array,nil] :scope The scope(s) to access
|
42
|
+
# @return [Google::Auth::ExternalAccount::AwsCredentials,
|
43
|
+
# Google::Auth::ExternalAccount::IdentityPoolCredentials,
|
44
|
+
# Google::Auth::ExternalAccount::PluggableAuthCredentials]
|
45
|
+
# The appropriate external account credentials based on the credential source
|
46
|
+
# @raise [Google::Auth::InitializationError] If the json file is missing, lacks required fields,
|
47
|
+
# or does not contain a supported credential source
|
40
48
|
def self.make_creds options = {}
|
41
49
|
json_key_io, scope = options.values_at :json_key_io, :scope
|
42
50
|
|
43
|
-
raise "A json file is required for external account credentials." unless json_key_io
|
51
|
+
raise InitializationError, "A json file is required for external account credentials." unless json_key_io
|
44
52
|
user_creds = read_json_key json_key_io
|
45
53
|
|
46
54
|
# AWS credentials is determined by aws subject token type
|
47
55
|
return make_aws_credentials user_creds, scope if user_creds[:subject_token_type] == AWS_SUBJECT_TOKEN_TYPE
|
48
56
|
|
49
|
-
raise MISSING_CREDENTIAL_SOURCE if user_creds[:credential_source].nil?
|
57
|
+
raise InitializationError, MISSING_CREDENTIAL_SOURCE if user_creds[:credential_source].nil?
|
50
58
|
user_creds[:scope] = scope
|
51
59
|
make_external_account_credentials user_creds
|
52
60
|
end
|
53
61
|
|
54
62
|
# Reads the required fields from the JSON.
|
63
|
+
#
|
64
|
+
# @param json_key_io [IO] An IO object containing the JSON key
|
65
|
+
# @return [Hash] The parsed JSON key
|
66
|
+
# @raise [Google::Auth::InitializationError] If the JSON is missing required fields
|
55
67
|
def self.read_json_key json_key_io
|
56
68
|
json_key = MultiJson.load json_key_io.read, symbolize_keys: true
|
57
69
|
wanted = [
|
58
70
|
:audience, :subject_token_type, :token_url, :credential_source
|
59
71
|
]
|
60
72
|
wanted.each do |key|
|
61
|
-
raise "the json is missing the #{key} field" unless json_key.key? key
|
73
|
+
raise InitializationError, "the json is missing the #{key} field" unless json_key.key? key
|
62
74
|
end
|
63
75
|
json_key
|
64
76
|
end
|
@@ -66,6 +78,11 @@ module Google
|
|
66
78
|
class << self
|
67
79
|
private
|
68
80
|
|
81
|
+
# Creates AWS credentials from the provided user credentials
|
82
|
+
#
|
83
|
+
# @param user_creds [Hash] The user credentials containing AWS credential source information
|
84
|
+
# @param scope [String,Array,nil] The scope(s) to access
|
85
|
+
# @return [Google::Auth::ExternalAccount::AwsCredentials] The AWS credentials
|
69
86
|
def make_aws_credentials user_creds, scope
|
70
87
|
Google::Auth::ExternalAccount::AwsCredentials.new(
|
71
88
|
audience: user_creds[:audience],
|
@@ -73,10 +90,18 @@ module Google
|
|
73
90
|
subject_token_type: user_creds[:subject_token_type],
|
74
91
|
token_url: user_creds[:token_url],
|
75
92
|
credential_source: user_creds[:credential_source],
|
76
|
-
service_account_impersonation_url: user_creds[:service_account_impersonation_url]
|
93
|
+
service_account_impersonation_url: user_creds[:service_account_impersonation_url],
|
94
|
+
universe_domain: user_creds[:universe_domain]
|
77
95
|
)
|
78
96
|
end
|
79
97
|
|
98
|
+
# Creates the appropriate external account credentials based on the credential source type
|
99
|
+
#
|
100
|
+
# @param user_creds [Hash] The user credentials containing credential source information
|
101
|
+
# @return [Google::Auth::ExternalAccount::IdentityPoolCredentials,
|
102
|
+
# Google::Auth::ExternalAccount::PluggableAuthCredentials]
|
103
|
+
# The appropriate external account credentials
|
104
|
+
# @raise [Google::Auth::InitializationError] If the credential source is not a supported type
|
80
105
|
def make_external_account_credentials user_creds
|
81
106
|
unless user_creds[:credential_source][:file].nil? && user_creds[:credential_source][:url].nil?
|
82
107
|
return Google::Auth::ExternalAccount::IdentityPoolCredentials.new user_creds
|
@@ -84,7 +109,7 @@ module Google
|
|
84
109
|
unless user_creds[:credential_source][:executable].nil?
|
85
110
|
return Google::Auth::ExternalAccount::PluggableAuthCredentials.new user_creds
|
86
111
|
end
|
87
|
-
raise INVALID_EXTERNAL_ACCOUNT_TYPE
|
112
|
+
raise InitializationError, INVALID_EXTERNAL_ACCOUNT_TYPE
|
88
113
|
end
|
89
114
|
end
|
90
115
|
end
|
@@ -24,7 +24,13 @@ module Google
|
|
24
24
|
module Connection
|
25
25
|
module_function
|
26
26
|
|
27
|
-
|
27
|
+
def default_connection
|
28
|
+
@default_connection
|
29
|
+
end
|
30
|
+
|
31
|
+
def default_connection= conn
|
32
|
+
@default_connection = conn
|
33
|
+
end
|
28
34
|
|
29
35
|
def connection
|
30
36
|
@default_connection || Faraday.default_connection
|
data/lib/googleauth/iam.rb
CHANGED
@@ -27,8 +27,9 @@ module Google
|
|
27
27
|
|
28
28
|
# Initializes an IAMCredentials.
|
29
29
|
#
|
30
|
-
# @param selector
|
31
|
-
# @param token
|
30
|
+
# @param selector [String] The IAM selector.
|
31
|
+
# @param token [String] The IAM token.
|
32
|
+
# @raise [TypeError] If selector or token is not a String
|
32
33
|
def initialize selector, token
|
33
34
|
raise TypeError unless selector.is_a? String
|
34
35
|
raise TypeError unless token.is_a? String
|
@@ -37,13 +38,19 @@ module Google
|
|
37
38
|
end
|
38
39
|
|
39
40
|
# Adds the credential fields to the hash.
|
41
|
+
#
|
42
|
+
# @param a_hash [Hash] The hash to update with credentials
|
43
|
+
# @return [Hash] The updated hash with credentials
|
40
44
|
def apply! a_hash
|
41
45
|
a_hash[SELECTOR_KEY] = @selector
|
42
46
|
a_hash[TOKEN_KEY] = @token
|
43
47
|
a_hash
|
44
48
|
end
|
45
49
|
|
46
|
-
# Returns a clone of a_hash updated with the
|
50
|
+
# Returns a clone of a_hash updated with the authorization header
|
51
|
+
#
|
52
|
+
# @param a_hash [Hash] The hash to clone and update with credentials
|
53
|
+
# @return [Hash] A new hash with credentials
|
47
54
|
def apply a_hash
|
48
55
|
a_copy = a_hash.clone
|
49
56
|
apply! a_copy
|
@@ -52,9 +59,18 @@ module Google
|
|
52
59
|
|
53
60
|
# Returns a reference to the #apply method, suitable for passing as
|
54
61
|
# a closure
|
62
|
+
#
|
63
|
+
# @return [Proc] A procedure that updates a hash with credentials
|
55
64
|
def updater_proc
|
56
65
|
proc { |a_hash, _opts = {}| apply a_hash }
|
57
66
|
end
|
67
|
+
|
68
|
+
# Returns the IAM authority selector as the principal
|
69
|
+
# @private
|
70
|
+
# @return [String] the IAM authoirty selector
|
71
|
+
def principal
|
72
|
+
@selector
|
73
|
+
end
|
58
74
|
end
|
59
75
|
end
|
60
76
|
end
|
@@ -14,6 +14,8 @@
|
|
14
14
|
# See the License for the specific language governing permissions and
|
15
15
|
# limitations under the License.
|
16
16
|
|
17
|
+
require "googleauth/errors"
|
18
|
+
|
17
19
|
|
18
20
|
module Google
|
19
21
|
module Auth
|
@@ -21,35 +23,39 @@ module Google
|
|
21
23
|
##
|
22
24
|
# Failed to obtain keys from the key source.
|
23
25
|
#
|
24
|
-
class KeySourceError < StandardError
|
26
|
+
class KeySourceError < StandardError
|
27
|
+
include Google::Auth::Error
|
28
|
+
end
|
25
29
|
|
26
30
|
##
|
27
31
|
# Failed to verify a token.
|
28
32
|
#
|
29
|
-
class VerificationError < StandardError
|
33
|
+
class VerificationError < StandardError
|
34
|
+
include Google::Auth::Error
|
35
|
+
end
|
30
36
|
|
31
37
|
##
|
32
|
-
# Failed to verify
|
38
|
+
# Failed to verify token because it is expired.
|
33
39
|
#
|
34
40
|
class ExpiredTokenError < VerificationError; end
|
35
41
|
|
36
42
|
##
|
37
|
-
# Failed to verify
|
43
|
+
# Failed to verify token because its signature did not match.
|
38
44
|
#
|
39
45
|
class SignatureError < VerificationError; end
|
40
46
|
|
41
47
|
##
|
42
|
-
# Failed to verify
|
48
|
+
# Failed to verify token because its issuer did not match.
|
43
49
|
#
|
44
50
|
class IssuerMismatchError < VerificationError; end
|
45
51
|
|
46
52
|
##
|
47
|
-
# Failed to verify
|
53
|
+
# Failed to verify token because its audience did not match.
|
48
54
|
#
|
49
55
|
class AudienceMismatchError < VerificationError; end
|
50
56
|
|
51
57
|
##
|
52
|
-
# Failed to verify
|
58
|
+
# Failed to verify token because its authorized party did not match.
|
53
59
|
#
|
54
60
|
class AuthorizedPartyMismatchError < VerificationError; end
|
55
61
|
end
|
@@ -72,8 +72,8 @@ module Google
|
|
72
72
|
#
|
73
73
|
# @param jwk [Hash,String] The JWK specification.
|
74
74
|
# @return [KeyInfo]
|
75
|
-
# @raise [KeySourceError] If the key could not be extracted from the
|
76
|
-
# JWK.
|
75
|
+
# @raise [Google::Auth::IDTokens::KeySourceError] If the key could not be extracted from the
|
76
|
+
# JWK due to invalid type, malformed JSON, or invalid key data.
|
77
77
|
#
|
78
78
|
def from_jwk jwk
|
79
79
|
jwk = symbolize_keys ensure_json_parsed jwk
|
@@ -94,10 +94,10 @@ module Google
|
|
94
94
|
# Create an array of KeyInfo from a JWK Set, which may be given as
|
95
95
|
# either a hash or an unparsed JSON string.
|
96
96
|
#
|
97
|
-
# @param
|
97
|
+
# @param jwk_set [Hash,String] The JWK Set specification.
|
98
98
|
# @return [Array<KeyInfo>]
|
99
|
-
# @raise [KeySourceError] If a key could not be extracted from the
|
100
|
-
# JWK Set.
|
99
|
+
# @raise [Google::Auth::IDTokens::KeySourceError] If a key could not be extracted from the
|
100
|
+
# JWK Set, or if the set contains no keys.
|
101
101
|
#
|
102
102
|
def from_jwk_set jwk_set
|
103
103
|
jwk_set = symbolize_keys ensure_json_parsed jwk_set
|
@@ -261,7 +261,8 @@ module Google
|
|
261
261
|
# return the new keys.
|
262
262
|
#
|
263
263
|
# @return [Array<KeyInfo>]
|
264
|
-
# @raise [KeySourceError]
|
264
|
+
# @raise [Google::Auth::IDTokens::KeySourceError] If key retrieval fails, JSON parsing
|
265
|
+
# fails, or the data cannot be interpreted as keys
|
265
266
|
#
|
266
267
|
def refresh_keys
|
267
268
|
@monitor.synchronize do
|
@@ -310,6 +311,11 @@ module Google
|
|
310
311
|
|
311
312
|
protected
|
312
313
|
|
314
|
+
# Interpret JSON data as X509 certificates
|
315
|
+
#
|
316
|
+
# @param data [Hash] The JSON data containing certificate strings
|
317
|
+
# @return [Array<KeyInfo>] Array of key info objects
|
318
|
+
# @raise [Google::Auth::IDTokens::KeySourceError] If X509 certificates cannot be parsed
|
313
319
|
def interpret_json data
|
314
320
|
data.map do |id, cert_str|
|
315
321
|
key = OpenSSL::X509::Certificate.new(cert_str).public_key
|
@@ -371,7 +377,7 @@ module Google
|
|
371
377
|
# Attempt to refresh keys and return the new keys.
|
372
378
|
#
|
373
379
|
# @return [Array<KeyInfo>]
|
374
|
-
# @raise [KeySourceError]
|
380
|
+
# @raise [Google::Auth::IDTokens::KeySourceError] If key retrieval failed for any source.
|
375
381
|
#
|
376
382
|
def refresh_keys
|
377
383
|
@sources.flat_map(&:refresh_keys)
|
@@ -61,10 +61,9 @@ module Google
|
|
61
61
|
# @param iss [String,nil] If given, override the `iss` check.
|
62
62
|
#
|
63
63
|
# @return [Hash] the decoded payload, if verification succeeded.
|
64
|
-
# @raise [KeySourceError] if the key source failed to obtain public keys
|
65
|
-
# @raise [VerificationError] if the token verification failed.
|
64
|
+
# @raise [Google::Auth::IDTokens::KeySourceError] if the key source failed to obtain public keys
|
65
|
+
# @raise [Google::Auth::IDTokens::VerificationError] if the token verification failed.
|
66
66
|
# Additional data may be available in the error subclass and message.
|
67
|
-
#
|
68
67
|
def verify token,
|
69
68
|
key_source: :default,
|
70
69
|
aud: :default,
|
data/lib/googleauth/id_tokens.rb
CHANGED
@@ -160,15 +160,14 @@ module Google
|
|
160
160
|
# checking is performed. Default is to check against {OIDC_ISSUERS}.
|
161
161
|
#
|
162
162
|
# @return [Hash] The decoded token payload.
|
163
|
-
# @raise [KeySourceError] if the key source failed to obtain public keys
|
164
|
-
# @raise [VerificationError] if the token verification failed.
|
163
|
+
# @raise [Google::Auth::IDTokens::KeySourceError] if the key source failed to obtain public keys
|
164
|
+
# @raise [Google::Auth::IDTokens::VerificationError] if the token verification failed.
|
165
165
|
# Additional data may be available in the error subclass and message.
|
166
166
|
#
|
167
167
|
def verify_oidc token,
|
168
168
|
aud: nil,
|
169
169
|
azp: nil,
|
170
170
|
iss: OIDC_ISSUERS
|
171
|
-
|
172
171
|
verifier = Verifier.new key_source: oidc_key_source,
|
173
172
|
aud: aud,
|
174
173
|
azp: azp,
|
@@ -198,15 +197,14 @@ module Google
|
|
198
197
|
# checking is performed. Default is to check against {IAP_ISSUERS}.
|
199
198
|
#
|
200
199
|
# @return [Hash] The decoded token payload.
|
201
|
-
# @raise [KeySourceError] if the key source failed to obtain public keys
|
202
|
-
# @raise [VerificationError] if the token verification failed.
|
200
|
+
# @raise [Google::Auth::IDTokens::KeySourceError] if the key source failed to obtain public keys
|
201
|
+
# @raise [Google::Auth::IDTokens::VerificationError] if the token verification failed.
|
203
202
|
# Additional data may be available in the error subclass and message.
|
204
203
|
#
|
205
204
|
def verify_iap token,
|
206
205
|
aud: nil,
|
207
206
|
azp: nil,
|
208
207
|
iss: IAP_ISSUERS
|
209
|
-
|
210
208
|
verifier = Verifier.new key_source: iap_key_source,
|
211
209
|
aud: aud,
|
212
210
|
azp: azp,
|
@@ -0,0 +1,329 @@
|
|
1
|
+
# Copyright 2024 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 "googleauth/base_client"
|
16
|
+
require "googleauth/errors"
|
17
|
+
require "googleauth/helpers/connection"
|
18
|
+
|
19
|
+
module Google
|
20
|
+
module Auth
|
21
|
+
# Authenticates requests using impersonation from base credentials.
|
22
|
+
# This is a two-step process: first authentication claim from the base credentials is created
|
23
|
+
# and then that claim is exchanged for a short-lived token at an IAMCredentials endpoint.
|
24
|
+
# The short-lived token and its expiration time are cached.
|
25
|
+
class ImpersonatedServiceAccountCredentials
|
26
|
+
# @private
|
27
|
+
ERROR_SUFFIX = <<~ERROR.freeze
|
28
|
+
when trying to get security access token
|
29
|
+
from IAM Credentials endpoint using the credentials provided.
|
30
|
+
ERROR
|
31
|
+
|
32
|
+
# @private
|
33
|
+
IAM_SCOPE = ["https://www.googleapis.com/auth/iam".freeze].freeze
|
34
|
+
|
35
|
+
# BaseClient most importantly implements the `:updater_proc` getter,
|
36
|
+
# that returns a reference to an `apply!` method that updates
|
37
|
+
# a hash argument provided with the authorization header containing
|
38
|
+
# the access token (impersonation token in this case).
|
39
|
+
include Google::Auth::BaseClient
|
40
|
+
|
41
|
+
include Helpers::Connection
|
42
|
+
|
43
|
+
# @return [Object] The original authenticated credentials used to fetch short-lived impersonation access tokens
|
44
|
+
attr_reader :base_credentials
|
45
|
+
|
46
|
+
# @return [Object] The modified version of base credentials, tailored for impersonation purposes
|
47
|
+
# with necessary scope adjustments
|
48
|
+
attr_reader :source_credentials
|
49
|
+
|
50
|
+
# @return [String] The URL endpoint used to generate an impersonation token. This URL should follow a specific
|
51
|
+
# format to specify the impersonated service account.
|
52
|
+
attr_reader :impersonation_url
|
53
|
+
|
54
|
+
# @return [Array<String>, String] The scope(s) required for the impersonated access token,
|
55
|
+
# indicating the permissions needed for the short-lived token
|
56
|
+
attr_reader :scope
|
57
|
+
|
58
|
+
# @return [String, nil] The short-lived impersonation access token, retrieved and cached
|
59
|
+
# after making the impersonation request
|
60
|
+
attr_reader :access_token
|
61
|
+
|
62
|
+
# @return [Time, nil] The expiration time of the current access token, used to determine
|
63
|
+
# if the token is still valid
|
64
|
+
attr_reader :expires_at
|
65
|
+
|
66
|
+
# Create a ImpersonatedServiceAccountCredentials
|
67
|
+
# When you use service account impersonation, you start with an authenticated principal
|
68
|
+
# (e.g. your user account or a service account)
|
69
|
+
# and request short-lived credentials for a service account
|
70
|
+
# that has the authorization that your use case requires.
|
71
|
+
#
|
72
|
+
# @param options [Hash] A hash of options to configure the credentials.
|
73
|
+
# @option options [Object] :base_credentials (required) The authenticated principal.
|
74
|
+
# It will be used as following:
|
75
|
+
# * will be duplicated (with IAM scope) to create the source credentials if it supports duplication
|
76
|
+
# * as source credentials otherwise.
|
77
|
+
# @option options [String] :impersonation_url (required) The URL to impersonate the service account.
|
78
|
+
# This URL should follow the format:
|
79
|
+
# `https://iamcredentials.{universe_domain}/v1/projects/-/serviceAccounts/{source_sa_email}:generateAccessToken`,
|
80
|
+
# where:
|
81
|
+
# - `{universe_domain}` is the domain of the IAMCredentials API endpoint (e.g., `googleapis.com`).
|
82
|
+
# - `{source_sa_email}` is the email address of the service account to impersonate.
|
83
|
+
# @option options [Array<String>, String] :scope (required) The scope(s) for the short-lived impersonation token,
|
84
|
+
# defining the permissions required for the token.
|
85
|
+
# @option options [Object] :source_credentials The authenticated principal that will be used
|
86
|
+
# to fetch the short-lived impersonation access token. It is an alternative to providing the base credentials.
|
87
|
+
#
|
88
|
+
# @return [Google::Auth::ImpersonatedServiceAccountCredentials]
|
89
|
+
def self.make_creds options = {}
|
90
|
+
new options
|
91
|
+
end
|
92
|
+
|
93
|
+
# Initializes a new instance of ImpersonatedServiceAccountCredentials.
|
94
|
+
#
|
95
|
+
# @param options [Hash] A hash of options to configure the credentials.
|
96
|
+
# @option options [Object] :base_credentials (required) The authenticated principal.
|
97
|
+
# It will be used as following:
|
98
|
+
# * will be duplicated (with IAM scope) to create the source credentials if it supports duplication
|
99
|
+
# * as source credentials otherwise.
|
100
|
+
# @option options [String] :impersonation_url (required) The URL to impersonate the service account.
|
101
|
+
# This URL should follow the format:
|
102
|
+
# `https://iamcredentials.{universe_domain}/v1/projects/-/serviceAccounts/{source_sa_email}:generateAccessToken`,
|
103
|
+
# where:
|
104
|
+
# - `{universe_domain}` is the domain of the IAMCredentials API endpoint (e.g., `googleapis.com`).
|
105
|
+
# - `{source_sa_email}` is the email address of the service account to impersonate.
|
106
|
+
# @option options [Array<String>, String] :scope (required) The scope(s) for the short-lived impersonation token,
|
107
|
+
# defining the permissions required for the token.
|
108
|
+
# @option options [Object] :source_credentials The authenticated principal that will be used
|
109
|
+
# to fetch the short-lived impersonation access token. It is an alternative to providing the base credentials.
|
110
|
+
# It is redundant to provide both source and base credentials as only source will be used,
|
111
|
+
# but it can be done, e.g. when duplicating existing credentials.
|
112
|
+
#
|
113
|
+
# @raise [ArgumentError] If any of the required options are missing.
|
114
|
+
#
|
115
|
+
# @return [Google::Auth::ImpersonatedServiceAccountCredentials]
|
116
|
+
def initialize options = {}
|
117
|
+
@base_credentials, @impersonation_url, @scope =
|
118
|
+
options.values_at :base_credentials,
|
119
|
+
:impersonation_url,
|
120
|
+
:scope
|
121
|
+
|
122
|
+
# Fail-fast checks for required parameters
|
123
|
+
if @base_credentials.nil? && !options.key?(:source_credentials)
|
124
|
+
raise ArgumentError, "Missing required option: either :base_credentials or :source_credentials"
|
125
|
+
end
|
126
|
+
raise ArgumentError, "Missing required option: :impersonation_url" if @impersonation_url.nil?
|
127
|
+
raise ArgumentError, "Missing required option: :scope" if @scope.nil?
|
128
|
+
|
129
|
+
# Some credentials (all Signet-based ones and this one) include scope and a bunch of transient state
|
130
|
+
# (e.g. refresh status) as part of themselves
|
131
|
+
# so a copy needs to be created with the scope overriden and transient state dropped.
|
132
|
+
#
|
133
|
+
# If a credentials does not support `duplicate` we'll try to use it as is assuming it has a broad enough scope.
|
134
|
+
# This might result in an "access denied" error downstream when the token from that credentials is being used
|
135
|
+
# for the token exchange.
|
136
|
+
@source_credentials = if options.key? :source_credentials
|
137
|
+
options[:source_credentials]
|
138
|
+
elsif @base_credentials.respond_to? :duplicate
|
139
|
+
@base_credentials.duplicate({
|
140
|
+
scope: IAM_SCOPE
|
141
|
+
})
|
142
|
+
else
|
143
|
+
@base_credentials
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Determines whether the current access token expires within the specified number of seconds.
|
148
|
+
#
|
149
|
+
# @param seconds [Integer] The number of seconds to check against the token's expiration time.
|
150
|
+
#
|
151
|
+
# @return [Boolean] Whether the access token expires within the given time frame
|
152
|
+
def expires_within? seconds
|
153
|
+
# This method is needed for BaseClient
|
154
|
+
@expires_at && @expires_at - Time.now.utc < seconds
|
155
|
+
end
|
156
|
+
|
157
|
+
# The universe domain of the impersonated credentials.
|
158
|
+
# Effectively this retrieves the universe domain of the source credentials.
|
159
|
+
#
|
160
|
+
# @return [String] The universe domain of the credentials.
|
161
|
+
def universe_domain
|
162
|
+
@source_credentials.universe_domain
|
163
|
+
end
|
164
|
+
|
165
|
+
# @return [Logger, nil] The logger of the credentials.
|
166
|
+
def logger
|
167
|
+
@source_credentials.logger if source_credentials.respond_to? :logger
|
168
|
+
end
|
169
|
+
|
170
|
+
# Creates a duplicate of these credentials without transient token state
|
171
|
+
#
|
172
|
+
# @param options [Hash] Overrides for the credentials parameters.
|
173
|
+
# The following keys are recognized
|
174
|
+
# * `base_credentials` the base credentials used to initialize the impersonation
|
175
|
+
# * `source_credentials` the authenticated credentials which usually would be
|
176
|
+
# base credentials with scope overridden to IAM_SCOPE
|
177
|
+
# * `impersonation_url` the URL to use to make an impersonation token exchange
|
178
|
+
# * `scope` the scope(s) to access
|
179
|
+
#
|
180
|
+
# @return [Google::Auth::ImpersonatedServiceAccountCredentials]
|
181
|
+
def duplicate options = {}
|
182
|
+
options = deep_hash_normalize options
|
183
|
+
|
184
|
+
options = {
|
185
|
+
base_credentials: @base_credentials,
|
186
|
+
source_credentials: @source_credentials,
|
187
|
+
impersonation_url: @impersonation_url,
|
188
|
+
scope: @scope
|
189
|
+
}.merge(options)
|
190
|
+
|
191
|
+
self.class.new options
|
192
|
+
end
|
193
|
+
|
194
|
+
# The principal behind the credentials. This class allows custom source credentials type
|
195
|
+
# that might not implement `principal`, in which case `:unknown` is returned.
|
196
|
+
#
|
197
|
+
# @private
|
198
|
+
# @return [String, Symbol] The string representation of the principal,
|
199
|
+
# the token type in lieu of the principal, or :unknown if source principal is unknown.
|
200
|
+
def principal
|
201
|
+
if @source_credentials.respond_to? :principal
|
202
|
+
@source_credentials.principal
|
203
|
+
else
|
204
|
+
:unknown
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
private
|
209
|
+
|
210
|
+
# Generates a new impersonation access token by exchanging the source credentials' token
|
211
|
+
# at the impersonation URL.
|
212
|
+
#
|
213
|
+
# This method first fetches an access token from the source credentials and then exchanges it
|
214
|
+
# for an impersonation token using the specified impersonation URL. The generated token and
|
215
|
+
# its expiration time are cached for subsequent use.
|
216
|
+
#
|
217
|
+
# @private
|
218
|
+
# @param _options [Hash] (optional) Additional options for token retrieval (currently unused).
|
219
|
+
#
|
220
|
+
# @raise [Google::Auth::UnexpectedStatusError] If the response status is 403 or 500.
|
221
|
+
# @raise [Google::Auth::AuthorizationError] For other unexpected response statuses.
|
222
|
+
#
|
223
|
+
# @return [String] The newly generated impersonation access token.
|
224
|
+
def fetch_access_token! _options = {}
|
225
|
+
auth_header = prepare_auth_header
|
226
|
+
resp = make_impersonation_request auth_header
|
227
|
+
|
228
|
+
case resp.status
|
229
|
+
when 200
|
230
|
+
response = MultiJson.load resp.body
|
231
|
+
self.expires_at = response["expireTime"]
|
232
|
+
@access_token = response["accessToken"]
|
233
|
+
access_token
|
234
|
+
when 403, 500
|
235
|
+
handle_error_response resp, UnexpectedStatusError
|
236
|
+
else
|
237
|
+
handle_error_response resp, AuthorizationError
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
# Prepares the authorization header for the impersonation request
|
242
|
+
# by fetching a token from source credentials.
|
243
|
+
#
|
244
|
+
# @private
|
245
|
+
# @return [Hash] The authorization header with the source credentials' token
|
246
|
+
def prepare_auth_header
|
247
|
+
auth_header = {}
|
248
|
+
@source_credentials.updater_proc.call auth_header
|
249
|
+
auth_header
|
250
|
+
end
|
251
|
+
|
252
|
+
# Makes the HTTP request to the impersonation endpoint.
|
253
|
+
#
|
254
|
+
# @private
|
255
|
+
# @param [Hash] auth_header The authorization header containing the source token
|
256
|
+
# @return [Faraday::Response] The HTTP response from the impersonation endpoint
|
257
|
+
def make_impersonation_request auth_header
|
258
|
+
connection.post @impersonation_url do |req|
|
259
|
+
req.headers.merge! auth_header
|
260
|
+
req.headers["Content-Type"] = "application/json"
|
261
|
+
req.body = MultiJson.dump({ scope: @scope })
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
# Creates and raises an appropriate error based on the response.
|
266
|
+
#
|
267
|
+
# @private
|
268
|
+
# @param [Faraday::Response] resp The HTTP response
|
269
|
+
# @param [Class] error_class The error class to instantiate
|
270
|
+
# @raise [StandardError] The appropriate error with details
|
271
|
+
def handle_error_response resp, error_class
|
272
|
+
msg = "Unexpected error code #{resp.status}.\n #{resp.env.response_body} #{ERROR_SUFFIX}"
|
273
|
+
raise error_class.with_details(
|
274
|
+
msg,
|
275
|
+
credential_type_name: self.class.name,
|
276
|
+
principal: principal
|
277
|
+
)
|
278
|
+
end
|
279
|
+
|
280
|
+
# Setter for the expires_at value that makes sure it is converted
|
281
|
+
# to Time object.
|
282
|
+
def expires_at= new_expires_at
|
283
|
+
@expires_at = normalize_timestamp new_expires_at
|
284
|
+
end
|
285
|
+
|
286
|
+
# Returns the type of token (access_token).
|
287
|
+
# This method is needed for BaseClient.
|
288
|
+
def token_type
|
289
|
+
:access_token
|
290
|
+
end
|
291
|
+
|
292
|
+
# Normalizes a timestamp to a Time object.
|
293
|
+
#
|
294
|
+
# @param time [Time, String, nil] The timestamp to normalize.
|
295
|
+
#
|
296
|
+
# @return [Time, nil] The normalized Time object, or nil if the input is nil.
|
297
|
+
#
|
298
|
+
# @raise [Google::Auth::CredentialsError] If the input is not a Time, String, or nil.
|
299
|
+
def normalize_timestamp time
|
300
|
+
case time
|
301
|
+
when NilClass
|
302
|
+
nil
|
303
|
+
when Time
|
304
|
+
time
|
305
|
+
when String
|
306
|
+
Time.parse time
|
307
|
+
else
|
308
|
+
message = "Invalid time value #{time}"
|
309
|
+
raise CredentialsError.with_details(message, credential_type_name: self.class.name, principal: principal)
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
# Convert all keys in this hash (nested) to symbols for uniform retrieval
|
314
|
+
def recursive_hash_normalize_keys val
|
315
|
+
if val.is_a? Hash
|
316
|
+
deep_hash_normalize val
|
317
|
+
else
|
318
|
+
val
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
def deep_hash_normalize old_hash
|
323
|
+
sym_hash = {}
|
324
|
+
old_hash&.each { |k, v| sym_hash[k.to_sym] = recursive_hash_normalize_keys v }
|
325
|
+
sym_hash
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|