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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +117 -0
  3. data/Credentials.md +106 -0
  4. data/Errors.md +152 -0
  5. data/README.md +49 -1
  6. data/lib/googleauth/api_key.rb +164 -0
  7. data/lib/googleauth/application_default.rb +6 -8
  8. data/lib/googleauth/base_client.rb +21 -4
  9. data/lib/googleauth/bearer_token.rb +162 -0
  10. data/lib/googleauth/client_id.rb +9 -6
  11. data/lib/googleauth/compute_engine.rb +231 -49
  12. data/lib/googleauth/credentials.rb +187 -58
  13. data/lib/googleauth/credentials_loader.rb +11 -20
  14. data/lib/googleauth/default_credentials.rb +29 -8
  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 +67 -6
  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 +32 -7
  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 -6
  28. data/lib/googleauth/impersonated_service_account.rb +329 -0
  29. data/lib/googleauth/json_key_reader.rb +13 -3
  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 +84 -104
  33. data/lib/googleauth/service_account_jwt_header.rb +187 -0
  34. data/lib/googleauth/signet.rb +169 -4
  35. data/lib/googleauth/token_store.rb +3 -3
  36. data/lib/googleauth/user_authorizer.rb +89 -11
  37. data/lib/googleauth/user_refresh.rb +72 -9
  38. data/lib/googleauth/version.rb +1 -1
  39. data/lib/googleauth/web_user_authorizer.rb +65 -17
  40. data/lib/googleauth.rb +8 -0
  41. metadata +45 -13
@@ -14,9 +14,11 @@
14
14
 
15
15
  require "forwardable"
16
16
  require "json"
17
+ require "pathname"
17
18
  require "signet/oauth_2/client"
18
19
 
19
20
  require "googleauth/credentials_loader"
21
+ require "googleauth/errors"
20
22
 
21
23
  module Google
22
24
  module Auth
@@ -26,6 +28,14 @@ module Google
26
28
  # In most cases, it is subclassed by API-specific credential classes that
27
29
  # can be instantiated by clients.
28
30
  #
31
+ # **Important:** If you accept a credential configuration (credential
32
+ # JSON/File/Stream) from an external source for authentication to Google
33
+ # Cloud, you must validate it before providing it to any Google API or
34
+ # library. Providing an unvalidated credential configuration to Google APIs
35
+ # can compromise the security of your systems and data. For more
36
+ # information, refer to [Validate credential configurations from external
37
+ # sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
38
+ #
29
39
  # ## Options
30
40
  #
31
41
  # Credentials classes are configured with options that dictate default
@@ -259,7 +269,7 @@ module Google
259
269
  # @return [Object] The value
260
270
  #
261
271
  def self.lookup_auth_param name, method_name = name
262
- val = instance_variable_get "@#{name}".to_sym
272
+ val = instance_variable_get :"@#{name}"
263
273
  val = yield if val.nil? && block_given?
264
274
  return val unless val.nil?
265
275
  return superclass.send method_name if superclass.respond_to? method_name
@@ -299,6 +309,12 @@ module Google
299
309
  #
300
310
  attr_reader :quota_project_id
301
311
 
312
+ # @private Temporary; remove when universe domain metadata endpoint is stable (see b/349488459).
313
+ def disable_universe_domain_check
314
+ return false unless @client.respond_to? :disable_universe_domain_check
315
+ @client.disable_universe_domain_check
316
+ end
317
+
302
318
  # @private Delegate client methods to the client object.
303
319
  extend Forwardable
304
320
 
@@ -315,9 +331,6 @@ module Google
315
331
  # @return [String, Array<String>] The scope for this client. A scope is an access range
316
332
  # defined by the authorization server. The scope can be a single value or a list of values.
317
333
  #
318
- # @!attribute [r] target_audience
319
- # @return [String] The final target audience for ID tokens returned by this credential.
320
- #
321
334
  # @!attribute [r] issuer
322
335
  # @return [String] The issuer ID associated with this client.
323
336
  #
@@ -328,43 +341,81 @@ module Google
328
341
  # @return [Proc] Returns a reference to the {Signet::OAuth2::Client#apply} method,
329
342
  # suitable for passing as a closure.
330
343
  #
344
+ # @!attribute [r] target_audience
345
+ # @return [String] The final target audience for ID tokens returned by this credential.
346
+ #
347
+ # @!attribute [rw] universe_domain
348
+ # @return [String] The universe domain issuing these credentials.
349
+ #
350
+ # @!attribute [rw] logger
351
+ # @return [Logger] The logger used to log credential operations such as token refresh.
352
+ #
331
353
  def_delegators :@client,
332
354
  :token_credential_uri, :audience,
333
- :scope, :issuer, :signing_key, :updater_proc, :target_audience
355
+ :scope, :issuer, :signing_key, :updater_proc, :target_audience,
356
+ :universe_domain, :universe_domain=, :logger, :logger=
334
357
 
335
358
  ##
336
359
  # Creates a new Credentials instance with the provided auth credentials, and with the default
337
360
  # values configured on the class.
338
361
  #
339
- # @param [String, Hash, Signet::OAuth2::Client] keyfile
340
- # The keyfile can be provided as one of the following:
362
+ # @param [String, Pathname, Hash, Google::Auth::BaseClient] source_creds
363
+ # The source of credentials. It can be provided as one of the following:
364
+ #
365
+ # * The path to a JSON keyfile (as a `String` or a `Pathname`)
366
+ # * The contents of a JSON keyfile (as a `Hash`)
367
+ # * A `Google::Auth::BaseClient` credentials object, including but not limited to
368
+ # a `Signet::OAuth2::Client` object.
369
+ # * Any credentials object that supports the methods this wrapper delegates to an inner client.
370
+ #
371
+ # If this parameter is an object (`Signet::OAuth2::Client` or other) it will be used as an inner client.
372
+ # Otherwise the inner client will be constructed from the JSON keyfile or the contens of the hash.
341
373
  #
342
- # * The path to a JSON keyfile (as a +String+)
343
- # * The contents of a JSON keyfile (as a +Hash+)
344
- # * A +Signet::OAuth2::Client+ object
345
374
  # @param [Hash] options
346
- # The options for configuring the credentials instance. The following is supported:
375
+ # The options for configuring this wrapper credentials object and the inner client.
376
+ # The options hash is used in two ways:
347
377
  #
348
- # * +:scope+ - the scope for the client
349
- # * +"project_id"+ (and optionally +"project"+) - the project identifier for the client
350
- # * +:connection_builder+ - the connection builder to use for the client
351
- # * +:default_connection+ - the default connection to use for the client
378
+ # 1. **Configuring the wrapper object:** Some options are used to directly
379
+ # configure the wrapper `Credentials` instance. These include:
380
+ #
381
+ # * `:project_id` (and optionally `:project`) - the project identifier for the client
382
+ # * `:quota_project_id` - the quota project identifier for the client
383
+ # * `:logger` - the logger used to log credential operations such as token refresh.
384
+ #
385
+ # 2. **Configuring the inner client:** When the `source_creds` parameter
386
+ # is a `String` or `Hash`, a new `Signet::OAuth2::Client` is created
387
+ # internally. The following options are used to configure this inner client:
388
+ #
389
+ # * `:scope` - the scope for the client
390
+ # * `:target_audience` - the target audience for the client
352
391
  #
353
- def initialize keyfile, options = {}
354
- verify_keyfile_provided! keyfile
355
- @project_id = options["project_id"] || options["project"]
356
- @quota_project_id = options["quota_project_id"]
357
- case keyfile
358
- when Google::Auth::BaseClient
359
- update_from_signet keyfile
392
+ # Any other options in the `options` hash are passed directly to the
393
+ # inner client constructor. This allows you to configure additional
394
+ # parameters of the `Signet::OAuth2::Client`, such as connection parameters,
395
+ # timeouts, etc.
396
+ #
397
+ # @raise [Google::Auth::InitializationError] If source_creds is nil
398
+ # @raise [ArgumentError] If both scope and target_audience are specified
399
+ #
400
+ def initialize source_creds, options = {}
401
+ if source_creds.nil?
402
+ raise InitializationError,
403
+ "The source credentials passed to Google::Auth::Credentials.new were nil."
404
+ end
405
+
406
+ options = symbolize_hash_keys options
407
+ @project_id = options[:project_id] || options[:project]
408
+ @quota_project_id = options[:quota_project_id]
409
+ case source_creds
410
+ when String, Pathname
411
+ update_from_filepath source_creds, options
360
412
  when Hash
361
- update_from_hash keyfile, options
413
+ update_from_hash source_creds, options
362
414
  else
363
- update_from_filepath keyfile, options
415
+ update_from_client source_creds
364
416
  end
365
- CredentialsLoader.warn_if_cloud_sdk_credentials @client.client_id
417
+ setup_logging logger: options.fetch(:logger, :default)
366
418
  @project_id ||= CredentialsLoader.load_gcloud_project_id
367
- @client.fetch_access_token! if @client.needs_access_token?
368
419
  @env_vars = nil
369
420
  @paths = nil
370
421
  @scope = nil
@@ -458,7 +509,8 @@ module Google
458
509
  audience: options[:audience] || audience
459
510
  }
460
511
  client = Google::Auth::DefaultCredentials.make_creds creds_input
461
- new client
512
+ options = options.select { |k, _v| k == :logger }
513
+ new client, options
462
514
  end
463
515
 
464
516
  private_class_method :from_env_vars,
@@ -466,25 +518,65 @@ module Google
466
518
  :from_application_default,
467
519
  :from_io
468
520
 
469
- protected
470
521
 
471
- # Verify that the keyfile argument is provided.
472
- def verify_keyfile_provided! keyfile
473
- return unless keyfile.nil?
474
- raise "The keyfile passed to Google::Auth::Credentials.new was nil."
522
+ # Creates a duplicate of these credentials. This method tries to create the duplicate of the
523
+ # wrapped credentials if they support duplication and use them as is if they don't.
524
+ #
525
+ # The wrapped credentials are typically `Signet::OAuth2::Client` objects and they keep
526
+ # the transient state (token, refresh token, etc). The duplication discards that state,
527
+ # allowing e.g. to get the token with a different scope.
528
+ #
529
+ # @param options [Hash] Overrides for the credentials parameters.
530
+ #
531
+ # The options hash is used in two ways:
532
+ #
533
+ # 1. **Configuring the duplicate of the wrapper object:** Some options are used to directly
534
+ # configure the wrapper `Credentials` instance. These include:
535
+ #
536
+ # * `:project_id` (and optionally `:project`) - the project identifier for the credentials
537
+ # * `:quota_project_id` - the quota project identifier for the credentials
538
+ #
539
+ # 2. **Configuring the duplicate of the inner client:** If the inner client supports duplication
540
+ # the options hash is passed to it. This allows for configuration of additional parameters,
541
+ # most importantly (but not limited to) the following:
542
+ #
543
+ # * `:scope` - the scope for the client
544
+ #
545
+ # @return [Credentials]
546
+ def duplicate options = {}
547
+ options = deep_hash_normalize options
548
+
549
+ options = {
550
+ project_id: @project_id,
551
+ quota_project_id: @quota_project_id
552
+ }.merge(options)
553
+
554
+ new_client = if @client.respond_to? :duplicate
555
+ @client.duplicate options
556
+ else
557
+ @client
558
+ end
559
+
560
+ self.class.new new_client, options
475
561
  end
476
562
 
563
+ protected
564
+
477
565
  # Verify that the keyfile argument is a file.
566
+ #
567
+ # @param [String] keyfile Path to the keyfile
568
+ # @raise [Google::Auth::InitializationError] If the keyfile does not exist
478
569
  def verify_keyfile_exists! keyfile
479
570
  exists = ::File.file? keyfile
480
- raise "The keyfile '#{keyfile}' is not a valid file." unless exists
571
+ raise InitializationError, "The keyfile '#{keyfile}' is not a valid file." unless exists
481
572
  end
482
573
 
483
574
  # Initializes the Signet client.
484
- def init_client keyfile, connection_options = {}
485
- client_opts = client_options keyfile
486
- Signet::OAuth2::Client.new(client_opts)
487
- .configure_connection(connection_options)
575
+ def init_client hash, options = {}
576
+ options = update_client_options options
577
+ io = StringIO.new JSON.generate hash
578
+ options.merge! json_key_io: io
579
+ Google::Auth::DefaultCredentials.make_creds options
488
580
  end
489
581
 
490
582
  # returns a new Hash with string keys instead of symbol keys.
@@ -492,42 +584,45 @@ module Google
492
584
  hash.to_h.transform_keys(&:to_s)
493
585
  end
494
586
 
495
- # rubocop:disable Metrics/AbcSize
587
+ # returns a new Hash with symbol keys instead of string keys.
588
+ def symbolize_hash_keys hash
589
+ hash.to_h.transform_keys(&:to_sym)
590
+ end
591
+
592
+ # Updates client options with defaults from the credential class
593
+ #
594
+ # @param [Hash] options Options to update
595
+ # @return [Hash] Updated options hash
596
+ # @raise [ArgumentError] If both scope and target_audience are specified
597
+ def update_client_options options
598
+ options = options.dup
496
599
 
497
- def client_options options
498
- # Keyfile options have higher priority over constructor defaults
499
- options["token_credential_uri"] ||= self.class.token_credential_uri
500
- options["audience"] ||= self.class.audience
501
- options["scope"] ||= self.class.scope
502
- options["target_audience"] ||= self.class.target_audience
600
+ # options have higher priority over constructor defaults
601
+ options[:token_credential_uri] ||= self.class.token_credential_uri
602
+ options[:audience] ||= self.class.audience
603
+ options[:scope] ||= self.class.scope
604
+ options[:target_audience] ||= self.class.target_audience
503
605
 
504
- if !Array(options["scope"]).empty? && options["target_audience"]
606
+ if !Array(options[:scope]).empty? && options[:target_audience]
505
607
  raise ArgumentError, "Cannot specify both scope and target_audience"
506
608
  end
609
+ options.delete :scope unless options[:target_audience].nil?
507
610
 
508
- needs_scope = options["target_audience"].nil?
509
- # client options for initializing signet client
510
- { token_credential_uri: options["token_credential_uri"],
511
- audience: options["audience"],
512
- scope: (needs_scope ? Array(options["scope"]) : nil),
513
- target_audience: options["target_audience"],
514
- issuer: options["client_email"],
515
- signing_key: OpenSSL::PKey::RSA.new(options["private_key"]) }
611
+ options
516
612
  end
517
613
 
518
- # rubocop:enable Metrics/AbcSize
519
-
520
- def update_from_signet client
614
+ def update_from_client client
521
615
  @project_id ||= client.project_id if client.respond_to? :project_id
522
616
  @quota_project_id ||= client.quota_project_id if client.respond_to? :quota_project_id
523
617
  @client = client
524
618
  end
619
+ alias update_from_signet update_from_client
525
620
 
526
621
  def update_from_hash hash, options
527
622
  hash = stringify_hash_keys hash
528
623
  hash["scope"] ||= options[:scope]
529
624
  hash["target_audience"] ||= options[:target_audience]
530
- @project_id ||= (hash["project_id"] || hash["project"])
625
+ @project_id ||= hash["project_id"] || hash["project"]
531
626
  @quota_project_id ||= hash["quota_project_id"]
532
627
  @client = init_client hash, options
533
628
  end
@@ -537,10 +632,44 @@ module Google
537
632
  json = JSON.parse ::File.read(path)
538
633
  json["scope"] ||= options[:scope]
539
634
  json["target_audience"] ||= options[:target_audience]
540
- @project_id ||= (json["project_id"] || json["project"])
635
+ @project_id ||= json["project_id"] || json["project"]
541
636
  @quota_project_id ||= json["quota_project_id"]
542
637
  @client = init_client json, options
543
638
  end
639
+
640
+ def setup_logging logger: :default
641
+ return unless @client.respond_to? :logger=
642
+ logging_env = ENV["GOOGLE_SDK_RUBY_LOGGING_GEMS"].to_s.downcase
643
+ if ["false", "none"].include? logging_env
644
+ logger = nil
645
+ elsif @client.logger
646
+ logger = @client.logger
647
+ elsif logger == :default
648
+ logger = nil
649
+ if ["true", "all"].include?(logging_env) || logging_env.split(",").include?("googleauth")
650
+ formatter = Google::Logging::StructuredFormatter.new if Google::Cloud::Env.get.logging_agent_expected?
651
+ logger = Logger.new $stderr, progname: "googleauth", formatter: formatter
652
+ end
653
+ end
654
+ @client.logger = logger
655
+ end
656
+
657
+ private
658
+
659
+ # Convert all keys in this hash (nested) to symbols for uniform retrieval
660
+ def recursive_hash_normalize_keys val
661
+ if val.is_a? Hash
662
+ deep_hash_normalize val
663
+ else
664
+ val
665
+ end
666
+ end
667
+
668
+ def deep_hash_normalize old_hash
669
+ sym_hash = {}
670
+ old_hash&.each { |k, v| sym_hash[k.to_sym] = recursive_hash_normalize_keys v }
671
+ sym_hash
672
+ end
544
673
  end
545
674
  end
546
675
  end
@@ -15,6 +15,8 @@
15
15
  require "os"
16
16
  require "rbconfig"
17
17
 
18
+ require "googleauth/errors"
19
+
18
20
  module Google
19
21
  # Module Auth provides classes that provide Google-specific authorization
20
22
  # used to access Google APIs.
@@ -37,7 +39,7 @@ module Google
37
39
  AWS_SESSION_TOKEN_VAR = "AWS_SESSION_TOKEN".freeze
38
40
  GCLOUD_POSIX_COMMAND = "gcloud".freeze
39
41
  GCLOUD_WINDOWS_COMMAND = "gcloud.cmd".freeze
40
- GCLOUD_CONFIG_COMMAND = "config config-helper --format json --verbosity none".freeze
42
+ GCLOUD_CONFIG_COMMAND = "config config-helper --format json --verbosity none --quiet".freeze
41
43
 
42
44
  CREDENTIALS_FILE_NAME = "application_default_credentials.json".freeze
43
45
  NOT_FOUND_ERROR = "Unable to read the credential file specified by #{ENV_VAR}".freeze
@@ -49,14 +51,6 @@ module Google
49
51
  CLOUD_SDK_CLIENT_ID = "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.app" \
50
52
  "s.googleusercontent.com".freeze
51
53
 
52
- CLOUD_SDK_CREDENTIALS_WARNING =
53
- "You are authenticating using user credentials." \
54
- "For production, we recommend using service account credentials." \
55
- "To learn more about service account credentials, see" \
56
- "http://cloud.google.com/docs/authentication/external/set-up-adc-on-cloud " \
57
- "To suppress this message, set the " \
58
- "GOOGLE_AUTH_SUPPRESS_CREDENTIALS_WARNINGS environment variable.".freeze
59
-
60
54
  # make_creds proxies the construction of a credentials instance
61
55
  #
62
56
  # By default, it calls #new on the current class, but this behaviour can
@@ -79,11 +73,12 @@ module Google
79
73
  # The following keys are recognized:
80
74
  # * `:default_connection` The connection object to use.
81
75
  # * `:connection_builder` A `Proc` that returns a connection.
76
+ # @raise [Google::Auth::InitializationError] If the credentials file cannot be read
82
77
  def from_env scope = nil, options = {}
83
78
  options = interpret_options scope, options
84
79
  if ENV.key?(ENV_VAR) && !ENV[ENV_VAR].empty?
85
80
  path = ENV[ENV_VAR]
86
- raise "file #{path} does not exist" unless File.exist? path
81
+ raise InitializationError, "file #{path} does not exist" unless File.exist? path
87
82
  File.open path do |f|
88
83
  return make_creds options.merge(json_key_io: f)
89
84
  end
@@ -91,7 +86,7 @@ module Google
91
86
  make_creds options
92
87
  end
93
88
  rescue StandardError => e
94
- raise "#{NOT_FOUND_ERROR}: #{e}"
89
+ raise InitializationError, "#{NOT_FOUND_ERROR}: #{e}"
95
90
  end
96
91
 
97
92
  # Creates an instance from a well known path.
@@ -105,6 +100,7 @@ module Google
105
100
  # The following keys are recognized:
106
101
  # * `:default_connection` The connection object to use.
107
102
  # * `:connection_builder` A `Proc` that returns a connection.
103
+ # @raise [Google::Auth::InitializationError] If the credentials file cannot be read
108
104
  def from_well_known_path scope = nil, options = {}
109
105
  options = interpret_options scope, options
110
106
  home_var = OS.windows? ? "APPDATA" : "HOME"
@@ -117,7 +113,7 @@ module Google
117
113
  return make_creds options.merge(json_key_io: f)
118
114
  end
119
115
  rescue StandardError => e
120
- raise "#{WELL_KNOWN_ERROR}: #{e}"
116
+ raise InitializationError, "#{WELL_KNOWN_ERROR}: #{e}"
121
117
  end
122
118
 
123
119
  # Creates an instance from the system default path
@@ -131,6 +127,7 @@ module Google
131
127
  # The following keys are recognized:
132
128
  # * `:default_connection` The connection object to use.
133
129
  # * `:connection_builder` A `Proc` that returns a connection.
130
+ # @raise [Google::Auth::InitializationError] If the credentials file cannot be read or is invalid
134
131
  def from_system_default_path scope = nil, options = {}
135
132
  options = interpret_options scope, options
136
133
  if OS.windows?
@@ -145,22 +142,16 @@ module Google
145
142
  return make_creds options.merge(json_key_io: f)
146
143
  end
147
144
  rescue StandardError => e
148
- raise "#{SYSTEM_DEFAULT_ERROR}: #{e}"
145
+ raise InitializationError, "#{SYSTEM_DEFAULT_ERROR}: #{e}"
149
146
  end
150
147
 
151
148
  module_function
152
149
 
153
- # Issues warning if cloud sdk client id is used
154
- def warn_if_cloud_sdk_credentials client_id
155
- return if ENV["GOOGLE_AUTH_SUPPRESS_CREDENTIALS_WARNINGS"]
156
- warn CLOUD_SDK_CREDENTIALS_WARNING if client_id == CLOUD_SDK_CLIENT_ID
157
- end
158
-
159
150
  # Finds project_id from gcloud CLI configuration
160
151
  def load_gcloud_project_id
161
152
  gcloud = GCLOUD_WINDOWS_COMMAND if OS.windows?
162
153
  gcloud = GCLOUD_POSIX_COMMAND unless OS.windows?
163
- gcloud_json = IO.popen("#{gcloud} #{GCLOUD_CONFIG_COMMAND}", in: :close, err: :close, &:read)
154
+ gcloud_json = IO.popen("#{gcloud} #{GCLOUD_CONFIG_COMMAND}", err: :close, &:read)
164
155
  config = MultiJson.load gcloud_json
165
156
  config["configuration"]["properties"]["core"]["project"]
166
157
  rescue StandardError
@@ -16,9 +16,11 @@ require "multi_json"
16
16
  require "stringio"
17
17
 
18
18
  require "googleauth/credentials_loader"
19
+ require "googleauth/errors"
20
+ require "googleauth/external_account"
19
21
  require "googleauth/service_account"
22
+ require "googleauth/service_account_jwt_header"
20
23
  require "googleauth/user_refresh"
21
- require "googleauth/external_account"
22
24
 
23
25
  module Google
24
26
  # Module Auth provides classes that provide Google-specific authorization
@@ -29,26 +31,41 @@ module Google
29
31
  class DefaultCredentials
30
32
  extend CredentialsLoader
31
33
 
32
- # override CredentialsLoader#make_creds to use the class determined by
34
+ ##
35
+ # Override CredentialsLoader#make_creds to use the class determined by
33
36
  # loading the json.
37
+ #
38
+ # **Important:** If you accept a credential configuration (credential
39
+ # JSON/File/Stream) from an external source for authentication to Google
40
+ # Cloud, you must validate it before providing it to any Google API or
41
+ # library. Providing an unvalidated credential configuration to Google
42
+ # APIs can compromise the security of your systems and data. For more
43
+ # information, refer to [Validate credential configurations from external
44
+ # sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
45
+ #
46
+ # @param options [Hash] Options for creating the credentials
47
+ # @return [Google::Auth::Credentials] The credentials instance
48
+ # @raise [Google::Auth::InitializationError] If the credentials cannot be determined
34
49
  def self.make_creds options = {}
35
50
  json_key_io = options[:json_key_io]
36
51
  if json_key_io
37
52
  json_key, clz = determine_creds_class json_key_io
38
- warn_if_cloud_sdk_credentials json_key["client_id"]
39
53
  io = StringIO.new MultiJson.dump(json_key)
40
54
  clz.make_creds options.merge(json_key_io: io)
41
55
  else
42
- warn_if_cloud_sdk_credentials ENV[CredentialsLoader::CLIENT_ID_VAR]
43
56
  clz = read_creds
44
57
  clz.make_creds options
45
58
  end
46
59
  end
47
60
 
61
+ # Reads the credential type from environment and returns the appropriate class
62
+ #
63
+ # @return [Class] The credential class to use
64
+ # @raise [Google::Auth::InitializationError] If the credentials type is undefined or unsupported
48
65
  def self.read_creds
49
66
  env_var = CredentialsLoader::ACCOUNT_TYPE_VAR
50
67
  type = ENV[env_var]
51
- raise "#{env_var} is undefined in env" unless type
68
+ raise InitializationError, "#{env_var} is undefined in env" unless type
52
69
  case type
53
70
  when "service_account"
54
71
  ServiceAccountCredentials
@@ -57,15 +74,19 @@ module Google
57
74
  when "external_account"
58
75
  ExternalAccount::Credentials
59
76
  else
60
- raise "credentials type '#{type}' is not supported"
77
+ raise InitializationError, "credentials type '#{type}' is not supported"
61
78
  end
62
79
  end
63
80
 
64
81
  # Reads the input json and determines which creds class to use.
82
+ #
83
+ # @param json_key_io [IO] An IO object containing the JSON key
84
+ # @return [Array(Hash, Class)] The JSON key and the credential class to use
85
+ # @raise [Google::Auth::InitializationError] If the JSON is missing the type field or has an unsupported type
65
86
  def self.determine_creds_class json_key_io
66
87
  json_key = MultiJson.load json_key_io.read
67
88
  key = "type"
68
- raise "the json is missing the '#{key}' field" unless json_key.key? key
89
+ raise InitializationError, "the json is missing the '#{key}' field" unless json_key.key? key
69
90
  type = json_key[key]
70
91
  case type
71
92
  when "service_account"
@@ -75,7 +96,7 @@ module Google
75
96
  when "external_account"
76
97
  [json_key, ExternalAccount::Credentials]
77
98
  else
78
- raise "credentials type '#{type}' is not supported"
99
+ raise InitializationError, "credentials type '#{type}' is not supported"
79
100
  end
80
101
  end
81
102
  end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "signet/oauth_2/client"
4
+
5
+ # Copyright 2025 Google LLC
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ module Google
20
+ module Auth
21
+ ##
22
+ # Error mixin module for Google Auth errors
23
+ # All Google Auth errors should include this module
24
+ #
25
+ module Error; end
26
+
27
+ ##
28
+ # Mixin module that contains detailed error information
29
+ # typically this is available if credentials initialization
30
+ # succeeds and credentials object is valid
31
+ #
32
+ module DetailedError
33
+ include Error
34
+
35
+ # The type of the credentials that the error was originated from
36
+ # @return [String, nil] The class name of the credential that raised the error
37
+ attr_reader :credential_type_name
38
+
39
+ # The principal for the authentication flow. Typically obtained from credentials
40
+ # @return [String, Symbol, nil] The principal identifier associated with the credentials
41
+ attr_reader :principal
42
+
43
+ # All details passed in the options hash when creating the error
44
+ # @return [Hash] Additional details about the error
45
+ attr_reader :details
46
+
47
+ # @private
48
+ def self.included base
49
+ base.extend ClassMethods
50
+ end
51
+
52
+ # Class methods to be added to including classes
53
+ module ClassMethods
54
+ # Creates a new error with detailed information
55
+ # @param message [String] The error message
56
+ # @param credential_type_name [String] The credential type that raised the error
57
+ # @param principal [String, Symbol] The principal for the authentication flow
58
+ # @return [Error] The new error with details
59
+ def with_details message, credential_type_name:, principal:
60
+ new(message).tap do |error|
61
+ error.instance_variable_set :@credential_type_name, credential_type_name
62
+ error.instance_variable_set :@principal, principal
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ ##
69
+ # Error raised during Credentials initialization.
70
+ # All new code should use this instead of ArgumentError during initializtion.
71
+ #
72
+ class InitializationError < StandardError
73
+ include Error
74
+ end
75
+
76
+ ##
77
+ # Generic error raised during operation of Credentials
78
+ # This should be used for all purposes not covered by other errors.
79
+ #
80
+ class CredentialsError < StandardError
81
+ include DetailedError
82
+ end
83
+
84
+ ##
85
+ # An error indicating the remote server refused to authorize the client.
86
+ # Maintains backward compatibility with Signet.
87
+ #
88
+ # Should not be used in the new code, even when wrapping `Signet::AuthorizationError`.
89
+ # New code should use CredentialsError instead.
90
+ #
91
+ class AuthorizationError < Signet::AuthorizationError
92
+ include DetailedError
93
+ end
94
+
95
+ ##
96
+ # An error indicating that the server sent an unexpected http status.
97
+ # Maintains backward compatibility with Signet.
98
+ #
99
+ # Should not be used in the new code, even when wrapping `Signet::UnexpectedStatusError`.
100
+ # New code should use CredentialsError instead.
101
+ #
102
+ class UnexpectedStatusError < Signet::UnexpectedStatusError
103
+ include DetailedError
104
+ end
105
+
106
+ ##
107
+ # An error indicating the client failed to parse a value.
108
+ # Maintains backward compatibility with Signet.
109
+ #
110
+ # Should not be used in the new code, even when wrapping `Signet::ParseError`.
111
+ # New code should use CredentialsError instead.
112
+ #
113
+ class ParseError < Signet::ParseError
114
+ include DetailedError
115
+ end
116
+ end
117
+ end