googleauth 1.8.1 → 1.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -26,6 +26,14 @@ module Google
26
26
  # In most cases, it is subclassed by API-specific credential classes that
27
27
  # can be instantiated by clients.
28
28
  #
29
+ # **Important:** If you accept a credential configuration (credential
30
+ # JSON/File/Stream) from an external source for authentication to Google
31
+ # Cloud, you must validate it before providing it to any Google API or
32
+ # library. Providing an unvalidated credential configuration to Google APIs
33
+ # can compromise the security of your systems and data. For more
34
+ # information, refer to [Validate credential configurations from external
35
+ # sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
36
+ #
29
37
  # ## Options
30
38
  #
31
39
  # Credentials classes are configured with options that dictate default
@@ -259,7 +267,7 @@ module Google
259
267
  # @return [Object] The value
260
268
  #
261
269
  def self.lookup_auth_param name, method_name = name
262
- val = instance_variable_get "@#{name}".to_sym
270
+ val = instance_variable_get :"@#{name}"
263
271
  val = yield if val.nil? && block_given?
264
272
  return val unless val.nil?
265
273
  return superclass.send method_name if superclass.respond_to? method_name
@@ -299,6 +307,12 @@ module Google
299
307
  #
300
308
  attr_reader :quota_project_id
301
309
 
310
+ # @private Temporary; remove when universe domain metadata endpoint is stable (see b/349488459).
311
+ def disable_universe_domain_check
312
+ return false unless @client.respond_to? :disable_universe_domain_check
313
+ @client.disable_universe_domain_check
314
+ end
315
+
302
316
  # @private Delegate client methods to the client object.
303
317
  extend Forwardable
304
318
 
@@ -315,9 +329,6 @@ module Google
315
329
  # @return [String, Array<String>] The scope for this client. A scope is an access range
316
330
  # defined by the authorization server. The scope can be a single value or a list of values.
317
331
  #
318
- # @!attribute [r] target_audience
319
- # @return [String] The final target audience for ID tokens returned by this credential.
320
- #
321
332
  # @!attribute [r] issuer
322
333
  # @return [String] The issuer ID associated with this client.
323
334
  #
@@ -328,42 +339,74 @@ module Google
328
339
  # @return [Proc] Returns a reference to the {Signet::OAuth2::Client#apply} method,
329
340
  # suitable for passing as a closure.
330
341
  #
342
+ # @!attribute [r] target_audience
343
+ # @return [String] The final target audience for ID tokens returned by this credential.
344
+ #
345
+ # @!attribute [rw] universe_domain
346
+ # @return [String] The universe domain issuing these credentials.
347
+ #
348
+ # @!attribute [rw] logger
349
+ # @return [Logger] The logger used to log credential operations such as token refresh.
350
+ #
331
351
  def_delegators :@client,
332
352
  :token_credential_uri, :audience,
333
- :scope, :issuer, :signing_key, :updater_proc, :target_audience
353
+ :scope, :issuer, :signing_key, :updater_proc, :target_audience,
354
+ :universe_domain, :universe_domain=, :logger, :logger=
334
355
 
335
356
  ##
336
357
  # Creates a new Credentials instance with the provided auth credentials, and with the default
337
358
  # values configured on the class.
338
359
  #
339
- # @param [String, Hash, Signet::OAuth2::Client] keyfile
340
- # The keyfile can be provided as one of the following:
360
+ # @param [String, Hash, Signet::OAuth2::Client] source_creds
361
+ # The source of credentials. It can be provided as one of the following:
362
+ #
363
+ # * The path to a JSON keyfile (as a `String`)
364
+ # * The contents of a JSON keyfile (as a `Hash`)
365
+ # * A `Signet::OAuth2::Client` credentials object
366
+ # * Any credentials object that supports the methods this wrapper delegates to an inner client.
367
+ #
368
+ # If this parameter is an object (`Signet::OAuth2::Client` or other) it will be used as an inner client.
369
+ # Otherwise the inner client will be constructed from the JSON keyfile or the contens of the hash.
341
370
  #
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
371
  # @param [Hash] options
346
- # The options for configuring the credentials instance. The following is supported:
372
+ # The options for configuring this wrapper credentials object and the inner client.
373
+ # The options hash is used in two ways:
347
374
  #
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
375
+ # 1. **Configuring the wrapper object:** Some options are used to directly
376
+ # configure the wrapper `Credentials` instance. These include:
377
+ #
378
+ # * `:project_id` (and optionally `:project`) - the project identifier for the client
379
+ # * `:quota_project_id` - the quota project identifier for the client
380
+ # * `:logger` - the logger used to log credential operations such as token refresh.
381
+ #
382
+ # 2. **Configuring the inner client:** When the `source_creds` parameter
383
+ # is a `String` or `Hash`, a new `Signet::OAuth2::Client` is created
384
+ # internally. The following options are used to configure this inner client:
352
385
  #
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
386
+ # * `:scope` - the scope for the client
387
+ # * `:target_audience` - the target audience for the client
388
+ #
389
+ # Any other options in the `options` hash are passed directly to the
390
+ # inner client constructor. This allows you to configure additional
391
+ # parameters of the `Signet::OAuth2::Client`, such as connection parameters,
392
+ # timeouts, etc.
393
+ #
394
+ def initialize source_creds, options = {}
395
+ raise "The source credentials passed to Google::Auth::Credentials.new were nil." if source_creds.nil?
396
+
397
+ options = symbolize_hash_keys options
398
+ @project_id = options[:project_id] || options[:project]
399
+ @quota_project_id = options[:quota_project_id]
400
+ case source_creds
401
+ when String
402
+ update_from_filepath source_creds, options
360
403
  when Hash
361
- update_from_hash keyfile, options
404
+ update_from_hash source_creds, options
362
405
  else
363
- update_from_filepath keyfile, options
406
+ update_from_client source_creds
364
407
  end
408
+ setup_logging logger: options.fetch(:logger, :default)
365
409
  @project_id ||= CredentialsLoader.load_gcloud_project_id
366
- @client.fetch_access_token! if @client.needs_access_token?
367
410
  @env_vars = nil
368
411
  @paths = nil
369
412
  @scope = nil
@@ -457,7 +500,8 @@ module Google
457
500
  audience: options[:audience] || audience
458
501
  }
459
502
  client = Google::Auth::DefaultCredentials.make_creds creds_input
460
- new client
503
+ options = options.select { |k, _v| k == :logger }
504
+ new client, options
461
505
  end
462
506
 
463
507
  private_class_method :from_env_vars,
@@ -465,14 +509,50 @@ module Google
465
509
  :from_application_default,
466
510
  :from_io
467
511
 
468
- protected
469
512
 
470
- # Verify that the keyfile argument is provided.
471
- def verify_keyfile_provided! keyfile
472
- return unless keyfile.nil?
473
- raise "The keyfile passed to Google::Auth::Credentials.new was nil."
513
+ # Creates a duplicate of these credentials. This method tries to create the duplicate of the
514
+ # wrapped credentials if they support duplication and use them as is if they don't.
515
+ #
516
+ # The wrapped credentials are typically `Signet::OAuth2::Client` objects and they keep
517
+ # the transient state (token, refresh token, etc). The duplication discards that state,
518
+ # allowing e.g. to get the token with a different scope.
519
+ #
520
+ # @param options [Hash] Overrides for the credentials parameters.
521
+ #
522
+ # The options hash is used in two ways:
523
+ #
524
+ # 1. **Configuring the duplicate of the wrapper object:** Some options are used to directly
525
+ # configure the wrapper `Credentials` instance. These include:
526
+ #
527
+ # * `:project_id` (and optionally `:project`) - the project identifier for the credentials
528
+ # * `:quota_project_id` - the quota project identifier for the credentials
529
+ #
530
+ # 2. **Configuring the duplicate of the inner client:** If the inner client supports duplication
531
+ # the options hash is passed to it. This allows for configuration of additional parameters,
532
+ # most importantly (but not limited to) the following:
533
+ #
534
+ # * `:scope` - the scope for the client
535
+ #
536
+ # @return [Credentials]
537
+ def duplicate options = {}
538
+ options = deep_hash_normalize options
539
+
540
+ options = {
541
+ project_id: @project_id,
542
+ quota_project_id: @quota_project_id
543
+ }.merge(options)
544
+
545
+ new_client = if @client.respond_to? :duplicate
546
+ @client.duplicate options
547
+ else
548
+ @client
549
+ end
550
+
551
+ self.class.new new_client, options
474
552
  end
475
553
 
554
+ protected
555
+
476
556
  # Verify that the keyfile argument is a file.
477
557
  def verify_keyfile_exists! keyfile
478
558
  exists = ::File.file? keyfile
@@ -480,10 +560,11 @@ module Google
480
560
  end
481
561
 
482
562
  # Initializes the Signet client.
483
- def init_client keyfile, connection_options = {}
484
- client_opts = client_options keyfile
485
- Signet::OAuth2::Client.new(client_opts)
486
- .configure_connection(connection_options)
563
+ def init_client hash, options = {}
564
+ options = update_client_options options
565
+ io = StringIO.new JSON.generate hash
566
+ options.merge! json_key_io: io
567
+ Google::Auth::DefaultCredentials.make_creds options
487
568
  end
488
569
 
489
570
  # returns a new Hash with string keys instead of symbol keys.
@@ -491,42 +572,40 @@ module Google
491
572
  hash.to_h.transform_keys(&:to_s)
492
573
  end
493
574
 
494
- # rubocop:disable Metrics/AbcSize
575
+ # returns a new Hash with symbol keys instead of string keys.
576
+ def symbolize_hash_keys hash
577
+ hash.to_h.transform_keys(&:to_sym)
578
+ end
579
+
580
+ def update_client_options options
581
+ options = options.dup
495
582
 
496
- def client_options options
497
- # Keyfile options have higher priority over constructor defaults
498
- options["token_credential_uri"] ||= self.class.token_credential_uri
499
- options["audience"] ||= self.class.audience
500
- options["scope"] ||= self.class.scope
501
- options["target_audience"] ||= self.class.target_audience
583
+ # options have higher priority over constructor defaults
584
+ options[:token_credential_uri] ||= self.class.token_credential_uri
585
+ options[:audience] ||= self.class.audience
586
+ options[:scope] ||= self.class.scope
587
+ options[:target_audience] ||= self.class.target_audience
502
588
 
503
- if !Array(options["scope"]).empty? && options["target_audience"]
589
+ if !Array(options[:scope]).empty? && options[:target_audience]
504
590
  raise ArgumentError, "Cannot specify both scope and target_audience"
505
591
  end
592
+ options.delete :scope unless options[:target_audience].nil?
506
593
 
507
- needs_scope = options["target_audience"].nil?
508
- # client options for initializing signet client
509
- { token_credential_uri: options["token_credential_uri"],
510
- audience: options["audience"],
511
- scope: (needs_scope ? Array(options["scope"]) : nil),
512
- target_audience: options["target_audience"],
513
- issuer: options["client_email"],
514
- signing_key: OpenSSL::PKey::RSA.new(options["private_key"]) }
594
+ options
515
595
  end
516
596
 
517
- # rubocop:enable Metrics/AbcSize
518
-
519
- def update_from_signet client
597
+ def update_from_client client
520
598
  @project_id ||= client.project_id if client.respond_to? :project_id
521
599
  @quota_project_id ||= client.quota_project_id if client.respond_to? :quota_project_id
522
600
  @client = client
523
601
  end
602
+ alias update_from_signet update_from_client
524
603
 
525
604
  def update_from_hash hash, options
526
605
  hash = stringify_hash_keys hash
527
606
  hash["scope"] ||= options[:scope]
528
607
  hash["target_audience"] ||= options[:target_audience]
529
- @project_id ||= (hash["project_id"] || hash["project"])
608
+ @project_id ||= hash["project_id"] || hash["project"]
530
609
  @quota_project_id ||= hash["quota_project_id"]
531
610
  @client = init_client hash, options
532
611
  end
@@ -536,10 +615,44 @@ module Google
536
615
  json = JSON.parse ::File.read(path)
537
616
  json["scope"] ||= options[:scope]
538
617
  json["target_audience"] ||= options[:target_audience]
539
- @project_id ||= (json["project_id"] || json["project"])
618
+ @project_id ||= json["project_id"] || json["project"]
540
619
  @quota_project_id ||= json["quota_project_id"]
541
620
  @client = init_client json, options
542
621
  end
622
+
623
+ def setup_logging logger: :default
624
+ return unless @client.respond_to? :logger=
625
+ logging_env = ENV["GOOGLE_SDK_RUBY_LOGGING_GEMS"].to_s.downcase
626
+ if ["false", "none"].include? logging_env
627
+ logger = nil
628
+ elsif @client.logger
629
+ logger = @client.logger
630
+ elsif logger == :default
631
+ logger = nil
632
+ if ["true", "all"].include?(logging_env) || logging_env.split(",").include?("googleauth")
633
+ formatter = Google::Logging::StructuredFormatter.new if Google::Cloud::Env.get.logging_agent_expected?
634
+ logger = Logger.new $stderr, progname: "googleauth", formatter: formatter
635
+ end
636
+ end
637
+ @client.logger = logger
638
+ end
639
+
640
+ private
641
+
642
+ # Convert all keys in this hash (nested) to symbols for uniform retrieval
643
+ def recursive_hash_normalize_keys val
644
+ if val.is_a? Hash
645
+ deep_hash_normalize val
646
+ else
647
+ val
648
+ end
649
+ end
650
+
651
+ def deep_hash_normalize old_hash
652
+ sym_hash = {}
653
+ old_hash&.each { |k, v| sym_hash[k.to_sym] = recursive_hash_normalize_keys v }
654
+ sym_hash
655
+ end
543
656
  end
544
657
  end
545
658
  end
@@ -19,6 +19,7 @@ require "googleauth/credentials_loader"
19
19
  require "googleauth/service_account"
20
20
  require "googleauth/user_refresh"
21
21
  require "googleauth/external_account"
22
+ require "googleauth/impersonated_service_account"
22
23
 
23
24
  module Google
24
25
  # Module Auth provides classes that provide Google-specific authorization
@@ -29,8 +30,18 @@ module Google
29
30
  class DefaultCredentials
30
31
  extend CredentialsLoader
31
32
 
32
- # override CredentialsLoader#make_creds to use the class determined by
33
+ ##
34
+ # Override CredentialsLoader#make_creds to use the class determined by
33
35
  # loading the json.
36
+ #
37
+ # **Important:** If you accept a credential configuration (credential
38
+ # JSON/File/Stream) from an external source for authentication to Google
39
+ # Cloud, you must validate it before providing it to any Google API or
40
+ # library. Providing an unvalidated credential configuration to Google
41
+ # APIs can compromise the security of your systems and data. For more
42
+ # information, refer to [Validate credential configurations from external
43
+ # sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
44
+ #
34
45
  def self.make_creds options = {}
35
46
  json_key_io = options[:json_key_io]
36
47
  if json_key_io
@@ -42,6 +42,7 @@ module Google
42
42
 
43
43
  attr_reader :expires_at
44
44
  attr_accessor :access_token
45
+ attr_accessor :universe_domain
45
46
 
46
47
  def expires_within? seconds
47
48
  # This method is needed for BaseClient
@@ -75,7 +76,7 @@ module Google
75
76
  # The retrieved subject token.
76
77
  #
77
78
  def retrieve_subject_token!
78
- raise NotImplementedError
79
+ raise NoMethodError, "retrieve_subject_token! not implemented"
79
80
  end
80
81
 
81
82
  # Returns whether the credentials represent a workforce pool (True) or
@@ -85,8 +86,7 @@ module Google
85
86
  # true if the credentials represent a workforce pool.
86
87
  # false if they represent a workload.
87
88
  def is_workforce_pool?
88
- pattern = "//iam\.googleapis\.com/locations/[^/]+/workforcePools/"
89
- /#{pattern}/.match?(@audience || "")
89
+ %r{/iam\.googleapis\.com/locations/[^/]+/workforcePools/}.match?(@audience || "")
90
90
  end
91
91
 
92
92
  private
@@ -111,6 +111,7 @@ module Google
111
111
  @quota_project_id = options[:quota_project_id]
112
112
  @project_id = nil
113
113
  @workforce_pool_user_project = options[:workforce_pool_user_project]
114
+ @universe_domain = options[:universe_domain] || "googleapis.com"
114
115
 
115
116
  @expires_at = nil
116
117
  @access_token = nil
@@ -128,7 +129,7 @@ module Google
128
129
  if @client_id.nil? && @workforce_pool_user_project
129
130
  additional_options = { userProject: @workforce_pool_user_project }
130
131
  end
131
- @sts_client.exchange_token(
132
+ token_request = {
132
133
  audience: @audience,
133
134
  grant_type: STS_GRANT_TYPE,
134
135
  subject_token: retrieve_subject_token!,
@@ -136,10 +137,31 @@ module Google
136
137
  scopes: @service_account_impersonation_url ? IAM_SCOPE : @scope,
137
138
  requested_token_type: STS_REQUESTED_TOKEN_TYPE,
138
139
  additional_options: additional_options
139
- )
140
+ }
141
+ log_token_request token_request
142
+ @sts_client.exchange_token token_request
143
+ end
144
+
145
+ def log_token_request token_request
146
+ logger&.info do
147
+ Google::Logging::Message.from(
148
+ message: "Requesting access token from #{token_request[:grant_type]}",
149
+ "credentialsId" => object_id
150
+ )
151
+ end
152
+ logger&.debug do
153
+ digest = Digest::SHA256.hexdigest token_request[:subject_token].to_s
154
+ loggable_request = token_request.merge subject_token: "(sha256:#{digest})"
155
+ Google::Logging::Message.from(
156
+ message: "Request data",
157
+ "request" => loggable_request,
158
+ "credentialsId" => object_id
159
+ )
160
+ end
140
161
  end
141
162
 
142
163
  def get_impersonated_access_token token, _options = {}
164
+ log_impersonated_token_request token
143
165
  response = connection.post @service_account_impersonation_url do |req|
144
166
  req.headers["Authorization"] = "Bearer #{token}"
145
167
  req.headers["Content-Type"] = "application/json"
@@ -152,6 +174,16 @@ module Google
152
174
 
153
175
  MultiJson.load response.body
154
176
  end
177
+
178
+ def log_impersonated_token_request original_token
179
+ logger&.info do
180
+ digest = Digest::SHA256.hexdigest original_token
181
+ Google::Logging::Message.from(
182
+ message: "Requesting impersonated access token with original token (sha256:#{digest})",
183
+ "credentialsId" => object_id
184
+ )
185
+ end
186
+ end
155
187
  end
156
188
  end
157
189
  end
@@ -73,7 +73,8 @@ module Google
73
73
  subject_token_type: user_creds[:subject_token_type],
74
74
  token_url: user_creds[:token_url],
75
75
  credential_source: user_creds[:credential_source],
76
- service_account_impersonation_url: user_creds[:service_account_impersonation_url]
76
+ service_account_impersonation_url: user_creds[:service_account_impersonation_url],
77
+ universe_domain: user_creds[:universe_domain]
77
78
  )
78
79
  end
79
80
 
@@ -168,7 +168,6 @@ module Google
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,
@@ -206,7 +205,6 @@ module Google
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,