googleauth 1.12.2 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 34f510693238aa2d0ac63fb3d7a91f8685d34dc9962e33952220b6ee8845beef
4
- data.tar.gz: 97d0eb5127ac1c609740d8b422b2002bd8f983043ff1a11cc09130c3c650d30f
3
+ metadata.gz: 9e2eef22c1062f2fd2216760e90a1af9ece1b99838bccea486a7b4ce43be9734
4
+ data.tar.gz: 1e60ef4856de1e4f7a3d423f3953e5e39f86c7002216ad2d98ee46ec88aaaf25
5
5
  SHA512:
6
- metadata.gz: 821238707fdf60880359514bc2385a273b4d16fe6bc6bd8ad804fcd36d2255667ad4a4dd39e7fabbd5f422367208a7fc7b74949943cda444481a8da3edfd16e2
7
- data.tar.gz: 792636b6a808d5c93dc7a3883a7c7d1e62cbb3d8b33b76e4da025cdbdac8cbd915c1cd4c5e1e698a58173d6fb2840e7623bd8c2a5673edc69113b662486029c4
6
+ metadata.gz: d5a87f962d04eb59c9ae083a4d3c46fd54ef4d4c53f0571b3952f6d1a9d30e3af89fb2c50e04b118e301097850c9985c713968dc29c62c5d388d0d8a4c3d306d
7
+ data.tar.gz: fae67434766ed2d4bb67e2c7ba2784a9397843110c28d96368d368b1dd30229a1ea83c5ee05e70a712446781e67ca84149fbaa72dd4239eb349d6943ff42b9b3
data/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Release History
2
2
 
3
+ ### 1.14.0 (2025-03-14)
4
+
5
+ #### Features
6
+
7
+ * add API key credentials ([#520](https://github.com/googleapis/google-auth-library-ruby/issues/520))
8
+ * Add Bearer token credentials
9
+ * add BearerToken credentials ([#522](https://github.com/googleapis/google-auth-library-ruby/issues/522))
10
+ * Update minimum Ruby version to 3.0 ([#527](https://github.com/googleapis/google-auth-library-ruby/issues/527))
11
+ #### Bug Fixes
12
+
13
+ * Eliminated the "attribute accessor as module_function" warning ([#519](https://github.com/googleapis/google-auth-library-ruby/issues/519))
14
+ * Get the project_id from gcloud ([#479](https://github.com/googleapis/google-auth-library-ruby/issues/479))
15
+ * logger configuration in service account JWT header ([#525](https://github.com/googleapis/google-auth-library-ruby/issues/525))
16
+
17
+ ### 1.13.1 (2025-01-24)
18
+
19
+ #### Bug Fixes
20
+
21
+ * Signet client subclasses no longer make the update! method private ([#516](https://github.com/googleapis/google-auth-library-ruby/issues/516))
22
+
23
+ ### 1.13.0 (2025-01-22)
24
+
25
+ #### Features
26
+
27
+ * create impersonated service credentials ([#499](https://github.com/googleapis/google-auth-library-ruby/issues/499))
28
+ #### Documentation
29
+
30
+ * Include note about validating externally-provided credentials ([#512](https://github.com/googleapis/google-auth-library-ruby/issues/512))
31
+
3
32
  ### 1.12.2 (2024-12-19)
4
33
 
5
34
  #### Bug Fixes
data/README.md CHANGED
@@ -64,6 +64,15 @@ well as a web variant tailored toward Rack-based applications.
64
64
  The authorizers are intended for authorization use cases. For sign-on,
65
65
  see [Google Identity Platform](https://developers.google.com/identity/)
66
66
 
67
+ ## Important notes
68
+
69
+ If you accept a credential configuration (credential JSON/File/Stream) from an
70
+ external source for authentication to Google Cloud, you must validate it before
71
+ providing it to any Google API or library. Providing an unvalidated credential
72
+ configuration to Google APIs can compromise the security of your systems and data.
73
+ For more information, refer to [Validate credential configurations from external
74
+ sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
75
+
67
76
  ### Example (Web)
68
77
 
69
78
  ```ruby
@@ -256,7 +265,7 @@ Custom storage implementations can also be used. See
256
265
 
257
266
  ## Supported Ruby Versions
258
267
 
259
- This library is supported on Ruby 2.6+.
268
+ This library is supported on Ruby 3.0+.
260
269
 
261
270
  Google provides official support for Ruby versions that are actively supported
262
271
  by Ruby Core—that is, Ruby versions that are either in normal maintenance or
@@ -0,0 +1,155 @@
1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "googleauth/base_client"
16
+ require "googleauth/credentials_loader"
17
+
18
+ module Google
19
+ module Auth
20
+ ##
21
+ # Implementation of Google API Key authentication.
22
+ #
23
+ # API Keys are text strings. They don't have an associated JSON file.
24
+ #
25
+ # The end-user is managing their API Keys directly, not via
26
+ # an authentication library.
27
+ #
28
+ # API Keys provide project information for an API request.
29
+ # API Keys don't reference an IAM principal, they do not expire,
30
+ # and cannot be refreshed.
31
+ #
32
+ class APIKeyCredentials
33
+ include Google::Auth::BaseClient
34
+
35
+ # @private Authorization header key
36
+ API_KEY_HEADER = "x-goog-api-key".freeze
37
+
38
+ # @private Environment variable containing API key
39
+ API_KEY_VAR = "GOOGLE_API_KEY".freeze
40
+
41
+ # @return [String] The API key
42
+ attr_reader :api_key
43
+
44
+ # @return [String] The universe domain of the universe
45
+ # this API key is for
46
+ attr_accessor :universe_domain
47
+
48
+ class << self
49
+ # Creates an APIKeyCredentials from the environment.
50
+ # Checks the ENV['GOOGLE_API_KEY'] variable.
51
+ #
52
+ # @param [String] _scope
53
+ # The scope to use for OAuth. Not used by API key auth.
54
+ # @param [Hash] options
55
+ # The options to pass to the credentials instance
56
+ #
57
+ # @return [Google::Auth::APIKeyCredentials, nil]
58
+ # Credentials if the API key environment variable is present,
59
+ # nil otherwise
60
+ def from_env _scope = nil, options = {}
61
+ api_key = ENV[API_KEY_VAR]
62
+ return nil if api_key.nil? || api_key.empty?
63
+ new options.merge(api_key: api_key)
64
+ end
65
+
66
+ # Create the APIKeyCredentials.
67
+ #
68
+ # @param [Hash] options The credentials options
69
+ # @option options [String] :api_key
70
+ # The API key to use for authentication
71
+ # @option options [String] :universe_domain
72
+ # The universe domain of the universe this API key
73
+ # belongs to (defaults to googleapis.com)
74
+ # @return [Google::Auth::APIKeyCredentials]
75
+ def make_creds options = {}
76
+ new options
77
+ end
78
+ end
79
+
80
+ # Initialize the APIKeyCredentials.
81
+ #
82
+ # @param [Hash] options The credentials options
83
+ # @option options [String] :api_key
84
+ # The API key to use for authentication
85
+ # @option options [String] :universe_domain
86
+ # The universe domain of the universe this API key
87
+ # belongs to (defaults to googleapis.com)
88
+ def initialize options = {}
89
+ raise ArgumentError, "API key must be provided" if options[:api_key].nil? || options[:api_key].empty?
90
+ @api_key = options[:api_key]
91
+ @universe_domain = options[:universe_domain] || "googleapis.com"
92
+ end
93
+
94
+ # Determines if the credentials object has expired.
95
+ # Since API keys don't expire, this always returns false.
96
+ #
97
+ # @param [Fixnum] _seconds
98
+ # The optional timeout in seconds since the last refresh
99
+ # @return [Boolean]
100
+ # True if the token has expired, false otherwise.
101
+ def expires_within? _seconds
102
+ false
103
+ end
104
+
105
+ # Creates a duplicate of these credentials.
106
+ #
107
+ # @param [Hash] options Additional options for configuring the credentials
108
+ # @return [Google::Auth::APIKeyCredentials]
109
+ def duplicate options = {}
110
+ self.class.new(
111
+ api_key: options[:api_key] || @api_key,
112
+ universe_domain: options[:universe_domain] || @universe_domain
113
+ )
114
+ end
115
+
116
+ # Updates the provided hash with the API Key header.
117
+ #
118
+ # The `apply!` method modifies the provided hash in place, adding the
119
+ # `x-goog-api-key` header with the API Key value.
120
+ #
121
+ # The API Key is hashed before being logged for security purposes.
122
+ #
123
+ # NB: this method typically would be called through `updater_proc`.
124
+ # Some older clients call it directly though, so it has to be public.
125
+ #
126
+ # @param [Hash] a_hash The hash to which the API Key header should be added.
127
+ # This is typically a hash representing the request headers. This hash
128
+ # will be modified in place.
129
+ # @param [Hash] _opts Additional options (currently not used). Included
130
+ # for consistency with the `BaseClient` interface.
131
+ # @return [Hash] The modified hash (the same hash passed as the `a_hash`
132
+ # argument).
133
+ def apply! a_hash, _opts = {}
134
+ a_hash[API_KEY_HEADER] = @api_key
135
+ logger&.debug do
136
+ hash = Digest::SHA256.hexdigest @api_key
137
+ Google::Logging::Message.from message: "Sending API key auth token. (sha256:#{hash})"
138
+ end
139
+ a_hash
140
+ end
141
+
142
+ protected
143
+
144
+ # The token type should be :api_key
145
+ def token_type
146
+ :api_key
147
+ end
148
+
149
+ # We don't need to fetch access tokens for API key auth
150
+ def fetch_access_token! _options = {}
151
+ nil
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,148 @@
1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "googleauth/base_client"
16
+
17
+ module Google
18
+ module Auth
19
+ ##
20
+ # Implementation of Bearer Token authentication scenario.
21
+ #
22
+ # Bearer tokens are strings representing an authorization grant.
23
+ # They can be OAuth2 ("ya.29") tokens, JWTs, IDTokens -- anything
24
+ # that is sent as a `Bearer` in an `Authorization` header.
25
+ #
26
+ # Not all 'authentication' strings can be used with this class,
27
+ # e.g. an API key cannot since API keys are sent in a
28
+ # `x-goog-api-key` header or as a query parameter.
29
+ #
30
+ # This class should be used when the end-user is managing the
31
+ # authentication token separately, e.g. with a separate service.
32
+ # This means that tasks like tracking the lifetime of and
33
+ # refreshing the token are outside the scope of this class.
34
+ #
35
+ # There is no JSON representation for this type of credentials.
36
+ # If the end-user has credentials in JSON format they should typically
37
+ # use the corresponding credentials type, e.g. ServiceAccountCredentials
38
+ # with the service account JSON.
39
+ #
40
+ class BearerTokenCredentials
41
+ include Google::Auth::BaseClient
42
+
43
+ # @private Authorization header name
44
+ AUTH_METADATA_KEY = Google::Auth::BaseClient::AUTH_METADATA_KEY
45
+
46
+ # @return [String] The token to be sent as a part of Bearer claim
47
+ attr_reader :token
48
+ # The following aliasing is needed for BaseClient since it sends :token_type
49
+ alias bearer_token token
50
+
51
+ # @return [Time, nil] The token expiration time provided by the end-user.
52
+ attr_reader :expires_at
53
+
54
+ # @return [String] The universe domain of the universe
55
+ # this token is for
56
+ attr_accessor :universe_domain
57
+
58
+ class << self
59
+ # Create the BearerTokenCredentials.
60
+ #
61
+ # @param [Hash] options The credentials options
62
+ # @option options [String] :token The bearer token to use.
63
+ # @option options [Time, Numeric, nil] :expires_at The token expiration time provided by the end-user.
64
+ # Optional, for the end-user's convenience. Can be a Time object, a number of seconds since epoch.
65
+ # If `expires_at` is `nil`, it is treated as "token never expires".
66
+ # @option options [String] :universe_domain The universe domain of the universe
67
+ # this token is for (defaults to googleapis.com)
68
+ # @return [Google::Auth::BearerTokenCredentials]
69
+ def make_creds options = {}
70
+ new options
71
+ end
72
+ end
73
+
74
+ # Initialize the BearerTokenCredentials.
75
+ #
76
+ # @param [Hash] options The credentials options
77
+ # @option options [String] :token The bearer token to use.
78
+ # @option options [Time, Numeric, nil] :expires_at The token expiration time provided by the end-user.
79
+ # Optional, for the end-user's convenience. Can be a Time object, a number of seconds since epoch.
80
+ # If `expires_at` is `nil`, it is treated as "token never expires".
81
+ # @option options [String] :universe_domain The universe domain of the universe
82
+ # this token is for (defaults to googleapis.com)
83
+ def initialize options = {}
84
+ raise ArgumentError, "Bearer token must be provided" if options[:token].nil? || options[:token].empty?
85
+ @token = options[:token]
86
+ @expires_at = case options[:expires_at]
87
+ when Time
88
+ options[:expires_at]
89
+ when Numeric
90
+ Time.at options[:expires_at]
91
+ end
92
+
93
+ @universe_domain = options[:universe_domain] || "googleapis.com"
94
+ end
95
+
96
+ # Determines if the credentials object has expired.
97
+ #
98
+ # @param [Numeric] seconds The optional timeout in seconds.
99
+ # @return [Boolean] True if the token has expired, false otherwise, or
100
+ # if the expires_at was not provided.
101
+ def expires_within? seconds
102
+ return false if @expires_at.nil? # Treat nil expiration as "never expires"
103
+ Time.now + seconds >= @expires_at
104
+ end
105
+
106
+ # Creates a duplicate of these credentials.
107
+ #
108
+ # @param [Hash] options Additional options for configuring the credentials
109
+ # @option options [String] :token The bearer token to use.
110
+ # @option options [Time, Numeric] :expires_at The token expiration time. Can be a Time
111
+ # object or a number of seconds since epoch.
112
+ # @option options [String] :universe_domain The universe domain (defaults to googleapis.com)
113
+ # @return [Google::Auth::BearerTokenCredentials]
114
+ def duplicate options = {}
115
+ self.class.new(
116
+ token: options[:token] || @token,
117
+ expires_at: options[:expires_at] || @expires_at,
118
+ universe_domain: options[:universe_domain] || @universe_domain
119
+ )
120
+ end
121
+
122
+ protected
123
+
124
+ ##
125
+ # BearerTokenCredentials do not support fetching a new token.
126
+ #
127
+ # If the token has an expiration time and is expired, this method will
128
+ # raise an error.
129
+ #
130
+ # @param [Hash] _options Options for fetching a new token (not used).
131
+ # @return [nil] Always returns nil.
132
+ # @raise [StandardError] If the token is expired.
133
+ def fetch_access_token! _options = {}
134
+ if @expires_at && Time.now >= @expires_at
135
+ raise "Bearer token has expired."
136
+ end
137
+
138
+ nil
139
+ end
140
+
141
+ private
142
+
143
+ def token_type
144
+ :bearer_token
145
+ end
146
+ end
147
+ end
148
+ end
@@ -87,12 +87,30 @@ module Google
87
87
  def initialize options = {}
88
88
  # Override the constructor to remember whether the universe domain was
89
89
  # overridden by a constructor argument.
90
- @universe_domain_overridden = options["universe_domain"] || options[:universe_domain] ? true : false
90
+ @universe_domain_overridden = options["universe_domain"] || options[:universe_domain]
91
91
  # TODO: Remove when universe domain metadata endpoint is stable (see b/349488459).
92
92
  @disable_universe_domain_check = true
93
93
  super options
94
94
  end
95
95
 
96
+ # Creates a duplicate of these credentials
97
+ # without the Signet::OAuth2::Client-specific
98
+ # transient state (e.g. cached tokens)
99
+ #
100
+ # @param options [Hash] Overrides for the credentials parameters.
101
+ # The following keys are recognized in addition to keys in the
102
+ # Signet::OAuth2::Client
103
+ # * `:universe_domain_overridden` Whether the universe domain was
104
+ # overriden during credentials creation
105
+ def duplicate options = {}
106
+ options = deep_hash_normalize options
107
+ super(
108
+ {
109
+ universe_domain_overridden: @universe_domain_overridden
110
+ }.merge(options)
111
+ )
112
+ end
113
+
96
114
  # @private
97
115
  # Overrides universe_domain getter to fetch lazily if it hasn't been
98
116
  # fetched yet. This is necessary specifically for Compute Engine because
@@ -136,6 +154,27 @@ module Google
136
154
  end
137
155
  end
138
156
 
157
+ # Destructively updates these credentials.
158
+ #
159
+ # This method is called by `Signet::OAuth2::Client`'s constructor
160
+ #
161
+ # @param options [Hash] Overrides for the credentials parameters.
162
+ # The following keys are recognized in addition to keys in the
163
+ # Signet::OAuth2::Client
164
+ # * `:universe_domain_overridden` Whether the universe domain was
165
+ # overriden during credentials creation
166
+ # @return [Google::Auth::GCECredentials]
167
+ def update! options = {}
168
+ # Normalize all keys to symbols to allow indifferent access.
169
+ options = deep_hash_normalize options
170
+
171
+ @universe_domain_overridden = options[:universe_domain_overridden] if options.key? :universe_domain_overridden
172
+
173
+ super(options)
174
+
175
+ self
176
+ end
177
+
139
178
  private
140
179
 
141
180
  def log_fetch_query
@@ -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
@@ -321,9 +329,6 @@ module Google
321
329
  # @return [String, Array<String>] The scope for this client. A scope is an access range
322
330
  # defined by the authorization server. The scope can be a single value or a list of values.
323
331
  #
324
- # @!attribute [r] target_audience
325
- # @return [String] The final target audience for ID tokens returned by this credential.
326
- #
327
332
  # @!attribute [r] issuer
328
333
  # @return [String] The issuer ID associated with this client.
329
334
  #
@@ -334,6 +339,9 @@ module Google
334
339
  # @return [Proc] Returns a reference to the {Signet::OAuth2::Client#apply} method,
335
340
  # suitable for passing as a closure.
336
341
  #
342
+ # @!attribute [r] target_audience
343
+ # @return [String] The final target audience for ID tokens returned by this credential.
344
+ #
337
345
  # @!attribute [rw] universe_domain
338
346
  # @return [String] The universe domain issuing these credentials.
339
347
  #
@@ -349,33 +357,53 @@ module Google
349
357
  # Creates a new Credentials instance with the provided auth credentials, and with the default
350
358
  # values configured on the class.
351
359
  #
352
- # @param [String, Hash, Signet::OAuth2::Client] keyfile
353
- # 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:
354
362
  #
355
363
  # * The path to a JSON keyfile (as a `String`)
356
364
  # * The contents of a JSON keyfile (as a `Hash`)
357
- # * A `Signet::OAuth2::Client` object
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.
370
+ #
358
371
  # @param [Hash] options
359
- # 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:
360
374
  #
361
- # * `:scope` - the scope for the client
362
- # * `project_id` (and optionally `project`) - the project identifier for the client
363
- # * `:connection_builder` - the connection builder to use for the client
364
- # * `:default_connection` - the default connection to use for the client
365
- # * `:logger` - the logger used to log credential operations such as token refresh.
375
+ # 1. **Configuring the wrapper object:** Some options are used to directly
376
+ # configure the wrapper `Credentials` instance. These include:
366
377
  #
367
- def initialize keyfile, options = {}
368
- verify_keyfile_provided! keyfile
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:
385
+ #
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
+
369
397
  options = symbolize_hash_keys options
370
398
  @project_id = options[:project_id] || options[:project]
371
399
  @quota_project_id = options[:quota_project_id]
372
- case keyfile
373
- when Google::Auth::BaseClient
374
- update_from_signet keyfile
400
+ case source_creds
401
+ when String
402
+ update_from_filepath source_creds, options
375
403
  when Hash
376
- update_from_hash keyfile, options
404
+ update_from_hash source_creds, options
377
405
  else
378
- update_from_filepath keyfile, options
406
+ update_from_client source_creds
379
407
  end
380
408
  setup_logging logger: options.fetch(:logger, :default)
381
409
  @project_id ||= CredentialsLoader.load_gcloud_project_id
@@ -481,14 +509,50 @@ module Google
481
509
  :from_application_default,
482
510
  :from_io
483
511
 
484
- protected
485
512
 
486
- # Verify that the keyfile argument is provided.
487
- def verify_keyfile_provided! keyfile
488
- return unless keyfile.nil?
489
- 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
490
552
  end
491
553
 
554
+ protected
555
+
492
556
  # Verify that the keyfile argument is a file.
493
557
  def verify_keyfile_exists! keyfile
494
558
  exists = ::File.file? keyfile
@@ -530,11 +594,12 @@ module Google
530
594
  options
531
595
  end
532
596
 
533
- def update_from_signet client
597
+ def update_from_client client
534
598
  @project_id ||= client.project_id if client.respond_to? :project_id
535
599
  @quota_project_id ||= client.quota_project_id if client.respond_to? :quota_project_id
536
600
  @client = client
537
601
  end
602
+ alias update_from_signet update_from_client
538
603
 
539
604
  def update_from_hash hash, options
540
605
  hash = stringify_hash_keys hash
@@ -571,6 +636,23 @@ module Google
571
636
  end
572
637
  @client.logger = logger
573
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
574
656
  end
575
657
  end
576
658
  end
@@ -37,7 +37,7 @@ module Google
37
37
  AWS_SESSION_TOKEN_VAR = "AWS_SESSION_TOKEN".freeze
38
38
  GCLOUD_POSIX_COMMAND = "gcloud".freeze
39
39
  GCLOUD_WINDOWS_COMMAND = "gcloud.cmd".freeze
40
- GCLOUD_CONFIG_COMMAND = "config config-helper --format json --verbosity none".freeze
40
+ GCLOUD_CONFIG_COMMAND = "config config-helper --format json --verbosity none --quiet".freeze
41
41
 
42
42
  CREDENTIALS_FILE_NAME = "application_default_credentials.json".freeze
43
43
  NOT_FOUND_ERROR = "Unable to read the credential file specified by #{ENV_VAR}".freeze
@@ -146,7 +146,7 @@ module Google
146
146
  def load_gcloud_project_id
147
147
  gcloud = GCLOUD_WINDOWS_COMMAND if OS.windows?
148
148
  gcloud = GCLOUD_POSIX_COMMAND unless OS.windows?
149
- gcloud_json = IO.popen("#{gcloud} #{GCLOUD_CONFIG_COMMAND}", in: :close, err: :close, &:read)
149
+ gcloud_json = IO.popen("#{gcloud} #{GCLOUD_CONFIG_COMMAND}", err: :close, &:read)
150
150
  config = MultiJson.load gcloud_json
151
151
  config["configuration"]["properties"]["core"]["project"]
152
152
  rescue StandardError