googleauth 1.8.0 → 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,43 +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
365
- CredentialsLoader.warn_if_cloud_sdk_credentials @client.client_id
408
+ setup_logging logger: options.fetch(:logger, :default)
366
409
  @project_id ||= CredentialsLoader.load_gcloud_project_id
367
- @client.fetch_access_token! if @client.needs_access_token?
368
410
  @env_vars = nil
369
411
  @paths = nil
370
412
  @scope = nil
@@ -458,7 +500,8 @@ module Google
458
500
  audience: options[:audience] || audience
459
501
  }
460
502
  client = Google::Auth::DefaultCredentials.make_creds creds_input
461
- new client
503
+ options = options.select { |k, _v| k == :logger }
504
+ new client, options
462
505
  end
463
506
 
464
507
  private_class_method :from_env_vars,
@@ -466,14 +509,50 @@ module Google
466
509
  :from_application_default,
467
510
  :from_io
468
511
 
469
- protected
470
512
 
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."
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
475
552
  end
476
553
 
554
+ protected
555
+
477
556
  # Verify that the keyfile argument is a file.
478
557
  def verify_keyfile_exists! keyfile
479
558
  exists = ::File.file? keyfile
@@ -481,10 +560,11 @@ module Google
481
560
  end
482
561
 
483
562
  # 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)
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
488
568
  end
489
569
 
490
570
  # returns a new Hash with string keys instead of symbol keys.
@@ -492,42 +572,40 @@ module Google
492
572
  hash.to_h.transform_keys(&:to_s)
493
573
  end
494
574
 
495
- # 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
496
582
 
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
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
503
588
 
504
- if !Array(options["scope"]).empty? && options["target_audience"]
589
+ if !Array(options[:scope]).empty? && options[:target_audience]
505
590
  raise ArgumentError, "Cannot specify both scope and target_audience"
506
591
  end
592
+ options.delete :scope unless options[:target_audience].nil?
507
593
 
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"]) }
594
+ options
516
595
  end
517
596
 
518
- # rubocop:enable Metrics/AbcSize
519
-
520
- def update_from_signet client
597
+ def update_from_client client
521
598
  @project_id ||= client.project_id if client.respond_to? :project_id
522
599
  @quota_project_id ||= client.quota_project_id if client.respond_to? :quota_project_id
523
600
  @client = client
524
601
  end
602
+ alias update_from_signet update_from_client
525
603
 
526
604
  def update_from_hash hash, options
527
605
  hash = stringify_hash_keys hash
528
606
  hash["scope"] ||= options[:scope]
529
607
  hash["target_audience"] ||= options[:target_audience]
530
- @project_id ||= (hash["project_id"] || hash["project"])
608
+ @project_id ||= hash["project_id"] || hash["project"]
531
609
  @quota_project_id ||= hash["quota_project_id"]
532
610
  @client = init_client hash, options
533
611
  end
@@ -537,10 +615,44 @@ module Google
537
615
  json = JSON.parse ::File.read(path)
538
616
  json["scope"] ||= options[:scope]
539
617
  json["target_audience"] ||= options[:target_audience]
540
- @project_id ||= (json["project_id"] || json["project"])
618
+ @project_id ||= json["project_id"] || json["project"]
541
619
  @quota_project_id ||= json["quota_project_id"]
542
620
  @client = init_client json, options
543
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
544
656
  end
545
657
  end
546
658
  end
@@ -49,14 +49,6 @@ module Google
49
49
  CLOUD_SDK_CLIENT_ID = "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.app" \
50
50
  "s.googleusercontent.com".freeze
51
51
 
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
52
  # make_creds proxies the construction of a credentials instance
61
53
  #
62
54
  # By default, it calls #new on the current class, but this behaviour can
@@ -150,12 +142,6 @@ module Google
150
142
 
151
143
  module_function
152
144
 
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
145
  # Finds project_id from gcloud CLI configuration
160
146
  def load_gcloud_project_id
161
147
  gcloud = GCLOUD_WINDOWS_COMMAND if OS.windows?
@@ -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,17 +30,25 @@ 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
37
48
  json_key, clz = determine_creds_class json_key_io
38
- warn_if_cloud_sdk_credentials json_key["client_id"]
39
49
  io = StringIO.new MultiJson.dump(json_key)
40
50
  clz.make_creds options.merge(json_key_io: io)
41
51
  else
42
- warn_if_cloud_sdk_credentials ENV[CredentialsLoader::CLIENT_ID_VAR]
43
52
  clz = read_creds
44
53
  clz.make_creds options
45
54
  end
@@ -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,