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