googleauth 0.8.1 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.kokoro/build.bat +9 -1
  3. data/.kokoro/continuous/linux.cfg +12 -2
  4. data/.kokoro/continuous/osx.cfg +5 -0
  5. data/.kokoro/continuous/post.cfg +30 -0
  6. data/.kokoro/continuous/windows.cfg +27 -1
  7. data/.kokoro/presubmit/linux.cfg +11 -1
  8. data/.kokoro/presubmit/osx.cfg +5 -0
  9. data/.kokoro/presubmit/windows.cfg +27 -1
  10. data/.kokoro/release.cfg +42 -1
  11. data/.kokoro/trampoline.bat +10 -0
  12. data/.repo-metadata.json +5 -0
  13. data/.rubocop.yml +10 -2
  14. data/CHANGELOG.md +34 -0
  15. data/Gemfile +8 -3
  16. data/README.md +7 -12
  17. data/Rakefile +48 -5
  18. data/googleauth.gemspec +6 -3
  19. data/integration/helper.rb +31 -0
  20. data/integration/id_tokens/key_source_test.rb +74 -0
  21. data/lib/googleauth.rb +1 -0
  22. data/lib/googleauth/application_default.rb +1 -1
  23. data/lib/googleauth/compute_engine.rb +19 -17
  24. data/lib/googleauth/credentials.rb +318 -63
  25. data/lib/googleauth/credentials_loader.rb +10 -8
  26. data/lib/googleauth/id_tokens.rb +233 -0
  27. data/lib/googleauth/id_tokens/errors.rb +71 -0
  28. data/lib/googleauth/id_tokens/key_sources.rb +394 -0
  29. data/lib/googleauth/id_tokens/verifier.rb +144 -0
  30. data/lib/googleauth/json_key_reader.rb +6 -2
  31. data/lib/googleauth/service_account.rb +16 -7
  32. data/lib/googleauth/signet.rb +8 -5
  33. data/lib/googleauth/user_authorizer.rb +6 -1
  34. data/lib/googleauth/user_refresh.rb +2 -2
  35. data/lib/googleauth/version.rb +1 -1
  36. data/lib/googleauth/web_user_authorizer.rb +13 -8
  37. data/rakelib/devsite_builder.rb +45 -0
  38. data/rakelib/link_checker.rb +64 -0
  39. data/rakelib/repo_metadata.rb +59 -0
  40. data/spec/googleauth/apply_auth_examples.rb +28 -5
  41. data/spec/googleauth/compute_engine_spec.rb +25 -13
  42. data/spec/googleauth/credentials_spec.rb +366 -161
  43. data/spec/googleauth/service_account_spec.rb +23 -16
  44. data/spec/googleauth/signet_spec.rb +46 -7
  45. data/spec/googleauth/user_authorizer_spec.rb +21 -1
  46. data/spec/googleauth/user_refresh_spec.rb +1 -1
  47. data/spec/googleauth/web_user_authorizer_spec.rb +6 -0
  48. data/test/helper.rb +33 -0
  49. data/test/id_tokens/key_sources_test.rb +240 -0
  50. data/test/id_tokens/verifier_test.rb +269 -0
  51. metadata +46 -12
  52. data/.kokoro/windows.sh +0 -4
@@ -9,7 +9,7 @@ Gem::Specification.new do |gem|
9
9
  gem.version = Google::Auth::VERSION
10
10
  gem.authors = ["Tim Emiola"]
11
11
  gem.email = "temiola@google.com"
12
- gem.homepage = "https://github.com/google/google-auth-library-ruby"
12
+ gem.homepage = "https://github.com/googleapis/google-auth-library-ruby"
13
13
  gem.summary = "Google Auth Library for Ruby"
14
14
  gem.license = "Apache-2.0"
15
15
  gem.description = <<-DESCRIPTION
@@ -25,11 +25,14 @@ Gem::Specification.new do |gem|
25
25
  end
26
26
  gem.require_paths = ["lib"]
27
27
  gem.platform = Gem::Platform::RUBY
28
+ gem.required_ruby_version = ">= 2.4.0"
28
29
 
29
- gem.add_dependency "faraday", "~> 0.12"
30
+ gem.add_dependency "faraday", ">= 0.17.3", "< 2.0"
30
31
  gem.add_dependency "jwt", ">= 1.4", "< 3.0"
31
32
  gem.add_dependency "memoist", "~> 0.16"
32
33
  gem.add_dependency "multi_json", "~> 1.11"
33
34
  gem.add_dependency "os", ">= 0.9", "< 2.0"
34
- gem.add_dependency "signet", "~> 0.7"
35
+ gem.add_dependency "signet", "~> 0.14"
36
+
37
+ gem.add_development_dependency "yard", "~> 0.9"
35
38
  end
@@ -0,0 +1,31 @@
1
+ # Copyright 2020 Google LLC
2
+ #
3
+ # Redistribution and use in source and binary forms, with or without
4
+ # modification, are permitted provided that the following conditions are
5
+ # met:
6
+ #
7
+ # * Redistributions of source code must retain the above copyright
8
+ # notice, this list of conditions and the following disclaimer.
9
+ # * Redistributions in binary form must reproduce the above
10
+ # copyright notice, this list of conditions and the following disclaimer
11
+ # in the documentation and/or other materials provided with the
12
+ # distribution.
13
+ # * Neither the name of Google Inc. nor the names of its
14
+ # contributors may be used to endorse or promote products derived from
15
+ # this software without specific prior written permission.
16
+ #
17
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
+
29
+ require "minitest/autorun"
30
+ require "minitest/focus"
31
+ require "googleauth"
@@ -0,0 +1,74 @@
1
+ # Copyright 2020 Google LLC
2
+ #
3
+ # Redistribution and use in source and binary forms, with or without
4
+ # modification, are permitted provided that the following conditions are
5
+ # met:
6
+ #
7
+ # * Redistributions of source code must retain the above copyright
8
+ # notice, this list of conditions and the following disclaimer.
9
+ # * Redistributions in binary form must reproduce the above
10
+ # copyright notice, this list of conditions and the following disclaimer
11
+ # in the documentation and/or other materials provided with the
12
+ # distribution.
13
+ # * Neither the name of Google Inc. nor the names of its
14
+ # contributors may be used to endorse or promote products derived from
15
+ # this software without specific prior written permission.
16
+ #
17
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
+
29
+ require "helper"
30
+
31
+ describe Google::Auth::IDTokens do
32
+ describe "key source" do
33
+ let(:legacy_oidc_key_source) {
34
+ Google::Auth::IDTokens::X509CertHttpKeySource.new "https://www.googleapis.com/oauth2/v1/certs"
35
+ }
36
+ let(:oidc_key_source) { Google::Auth::IDTokens.oidc_key_source }
37
+ let(:iap_key_source) { Google::Auth::IDTokens.iap_key_source }
38
+
39
+ it "Gets real keys from the OAuth2 V1 cert URL" do
40
+ keys = legacy_oidc_key_source.refresh_keys
41
+ refute_empty keys
42
+ keys.each do |key|
43
+ assert_kind_of OpenSSL::PKey::RSA, key.key
44
+ refute key.key.private?
45
+ assert_equal "RS256", key.algorithm
46
+ end
47
+ end
48
+
49
+ it "Gets real keys from the OAuth2 V3 cert URL" do
50
+ keys = oidc_key_source.refresh_keys
51
+ refute_empty keys
52
+ keys.each do |key|
53
+ assert_kind_of OpenSSL::PKey::RSA, key.key
54
+ refute key.key.private?
55
+ assert_equal "RS256", key.algorithm
56
+ end
57
+ end
58
+
59
+ it "Gets the same keys from the OAuth2 V1 and V3 cert URLs" do
60
+ keys_v1 = legacy_oidc_key_source.refresh_keys.map(&:key).map(&:export).sort
61
+ keys_v3 = oidc_key_source.refresh_keys.map(&:key).map(&:export).sort
62
+ assert_equal keys_v1, keys_v3
63
+ end
64
+
65
+ it "Gets real keys from the IAP public key URL" do
66
+ keys = iap_key_source.refresh_keys
67
+ refute_empty keys
68
+ keys.each do |key|
69
+ assert_kind_of OpenSSL::PKey::EC, key.key
70
+ assert_equal "ES256", key.algorithm
71
+ end
72
+ end
73
+ end
74
+ end
@@ -31,5 +31,6 @@ require "googleauth/application_default"
31
31
  require "googleauth/client_id"
32
32
  require "googleauth/credentials"
33
33
  require "googleauth/default_credentials"
34
+ require "googleauth/id_tokens"
34
35
  require "googleauth/user_authorizer"
35
36
  require "googleauth/web_user_authorizer"
@@ -47,7 +47,7 @@ module Google
47
47
  #
48
48
  # Use this to obtain the Application Default Credentials for accessing
49
49
  # Google APIs. Application Default Credentials are described in detail
50
- # at http://goo.gl/IUuyuX.
50
+ # at https://cloud.google.com/docs/authentication/production.
51
51
  #
52
52
  # If supplied, scope is used to create the credentials instance, when it can
53
53
  # be applied. E.g, on google compute engine and for user credentials the
@@ -51,30 +51,26 @@ module Google
51
51
  class GCECredentials < Signet::OAuth2::Client
52
52
  # The IP Address is used in the URIs to speed up failures on non-GCE
53
53
  # systems.
54
- COMPUTE_AUTH_TOKEN_URI = "http://169.254.169.254/computeMetadata/v1/"\
55
- "instance/service-accounts/default/token".freeze
54
+ COMPUTE_AUTH_TOKEN_URI =
55
+ "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token".freeze
56
+ COMPUTE_ID_TOKEN_URI =
57
+ "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/identity".freeze
56
58
  COMPUTE_CHECK_URI = "http://169.254.169.254".freeze
57
59
 
58
60
  class << self
59
61
  extend Memoist
60
62
 
61
63
  # Detect if this appear to be a GCE instance, by checking if metadata
62
- # is available
64
+ # is available.
63
65
  def on_gce? options = {}
66
+ # TODO: This should use google-cloud-env instead.
64
67
  c = options[:connection] || Faraday.default_connection
65
- resp = c.get COMPUTE_CHECK_URI do |req|
66
- # Comment from: oauth2client/client.py
67
- #
68
- # Note: the explicit `timeout` below is a workaround. The underlying
69
- # issue is that resolving an unknown host on some networks will take
70
- # 20-30 seconds; making this timeout short fixes the issue, but
71
- # could lead to false negatives in the event that we are on GCE, but
72
- # the metadata resolution was particularly slow. The latter case is
73
- # "unlikely".
74
- req.options.timeout = 0.1
68
+ headers = { "Metadata-Flavor" => "Google" }
69
+ resp = c.get COMPUTE_CHECK_URI, nil, headers do |req|
70
+ req.options.timeout = 1.0
71
+ req.options.open_timeout = 0.1
75
72
  end
76
73
  return false unless resp.status == 200
77
- return false unless resp.headers.key? "Metadata-Flavor"
78
74
  resp.headers["Metadata-Flavor"] == "Google"
79
75
  rescue Faraday::TimeoutError, Faraday::ConnectionFailed
80
76
  false
@@ -88,12 +84,18 @@ module Google
88
84
  def fetch_access_token options = {}
89
85
  c = options[:connection] || Faraday.default_connection
90
86
  retry_with_error do
87
+ uri = target_audience ? COMPUTE_ID_TOKEN_URI : COMPUTE_AUTH_TOKEN_URI
88
+ query = target_audience ? { "audience" => target_audience, "format" => "full" } : nil
91
89
  headers = { "Metadata-Flavor" => "Google" }
92
- resp = c.get COMPUTE_AUTH_TOKEN_URI, nil, headers
90
+ resp = c.get uri, query, headers
93
91
  case resp.status
94
92
  when 200
95
- Signet::OAuth2.parse_credentials(resp.body,
96
- resp.headers["content-type"])
93
+ content_type = resp.headers["content-type"]
94
+ if content_type == "text/html"
95
+ { (target_audience ? "id_token" : "access_token") => resp.body }
96
+ else
97
+ Signet::OAuth2.parse_credentials resp.body, content_type
98
+ end
97
99
  when 404
98
100
  raise Signet::AuthorizationError, NO_METADATA_SERVER_ERROR
99
101
  else
@@ -35,63 +35,294 @@ require "googleauth/credentials_loader"
35
35
 
36
36
  module Google
37
37
  module Auth
38
- # This class is intended to be inherited by API-specific classes
39
- # which overrides the SCOPE constant.
38
+ ##
39
+ # Credentials is responsible for representing the authentication when connecting to an API. This
40
+ # class is also intended to be inherited by API-specific classes.
40
41
  class Credentials
42
+ ##
43
+ # The default token credential URI to be used when none is provided during initialization.
41
44
  TOKEN_CREDENTIAL_URI = "https://oauth2.googleapis.com/token".freeze
45
+
46
+ ##
47
+ # The default target audience ID to be used when none is provided during initialization.
42
48
  AUDIENCE = "https://oauth2.googleapis.com/token".freeze
43
- SCOPE = [].freeze
44
- PATH_ENV_VARS = [].freeze
45
- JSON_ENV_VARS = [].freeze
46
- DEFAULT_PATHS = [].freeze
47
49
 
50
+ @audience = @scope = @target_audience = @env_vars = @paths = nil
51
+
52
+ ##
53
+ # The default token credential URI to be used when none is provided during initialization.
54
+ # The URI is the authorization server's HTTP endpoint capable of issuing tokens and
55
+ # refreshing expired tokens.
56
+ #
57
+ # @return [String]
58
+ #
59
+ def self.token_credential_uri
60
+ return @token_credential_uri unless @token_credential_uri.nil?
61
+
62
+ const_get :TOKEN_CREDENTIAL_URI if const_defined? :TOKEN_CREDENTIAL_URI
63
+ end
64
+
65
+ ##
66
+ # Set the default token credential URI to be used when none is provided during initialization.
67
+ #
68
+ # @param [String] new_token_credential_uri
69
+ # @return [String]
70
+ #
71
+ def self.token_credential_uri= new_token_credential_uri
72
+ @token_credential_uri = new_token_credential_uri
73
+ end
74
+
75
+ ##
76
+ # The default target audience ID to be used when none is provided during initialization.
77
+ # Used only by the assertion grant type.
78
+ #
79
+ # @return [String]
80
+ #
81
+ def self.audience
82
+ return @audience unless @audience.nil?
83
+
84
+ const_get :AUDIENCE if const_defined? :AUDIENCE
85
+ end
86
+
87
+ ##
88
+ # Sets the default target audience ID to be used when none is provided during initialization.
89
+ #
90
+ # @param [String] new_audience
91
+ # @return [String]
92
+ #
93
+ def self.audience= new_audience
94
+ @audience = new_audience
95
+ end
96
+
97
+ ##
98
+ # The default scope to be used when none is provided during initialization.
99
+ # A scope is an access range defined by the authorization server.
100
+ # The scope can be a single value or a list of values.
101
+ #
102
+ # Either {#scope} or {#target_audience}, but not both, should be non-nil.
103
+ # If {#scope} is set, this credential will produce access tokens.
104
+ # If {#target_audience} is set, this credential will produce ID tokens.
105
+ #
106
+ # @return [String, Array<String>]
107
+ #
108
+ def self.scope
109
+ return @scope unless @scope.nil?
110
+
111
+ Array(const_get(:SCOPE)).flatten.uniq if const_defined? :SCOPE
112
+ end
113
+
114
+ ##
115
+ # Sets the default scope to be used when none is provided during initialization.
116
+ #
117
+ # Either {#scope} or {#target_audience}, but not both, should be non-nil.
118
+ # If {#scope} is set, this credential will produce access tokens.
119
+ # If {#target_audience} is set, this credential will produce ID tokens.
120
+ #
121
+ # @param [String, Array<String>] new_scope
122
+ # @return [String, Array<String>]
123
+ #
124
+ def self.scope= new_scope
125
+ new_scope = Array new_scope unless new_scope.nil?
126
+ @scope = new_scope
127
+ end
128
+
129
+ ##
130
+ # The default final target audience for ID tokens, to be used when none
131
+ # is provided during initialization.
132
+ #
133
+ # Either {#scope} or {#target_audience}, but not both, should be non-nil.
134
+ # If {#scope} is set, this credential will produce access tokens.
135
+ # If {#target_audience} is set, this credential will produce ID tokens.
136
+ #
137
+ # @return [String]
138
+ #
139
+ def self.target_audience
140
+ @target_audience
141
+ end
142
+
143
+ ##
144
+ # Sets the default final target audience for ID tokens, to be used when none
145
+ # is provided during initialization.
146
+ #
147
+ # Either {#scope} or {#target_audience}, but not both, should be non-nil.
148
+ # If {#scope} is set, this credential will produce access tokens.
149
+ # If {#target_audience} is set, this credential will produce ID tokens.
150
+ #
151
+ # @param [String] new_target_audience
152
+ #
153
+ def self.target_audience= new_target_audience
154
+ @target_audience = new_target_audience
155
+ end
156
+
157
+ ##
158
+ # The environment variables to search for credentials. Values can either be a file path to the
159
+ # credentials file, or the JSON contents of the credentials file.
160
+ #
161
+ # @return [Array<String>]
162
+ #
163
+ def self.env_vars
164
+ return @env_vars unless @env_vars.nil?
165
+
166
+ # Pull values when PATH_ENV_VARS or JSON_ENV_VARS constants exists.
167
+ tmp_env_vars = []
168
+ tmp_env_vars << const_get(:PATH_ENV_VARS) if const_defined? :PATH_ENV_VARS
169
+ tmp_env_vars << const_get(:JSON_ENV_VARS) if const_defined? :JSON_ENV_VARS
170
+ tmp_env_vars.flatten.uniq
171
+ end
172
+
173
+ ##
174
+ # Sets the environment variables to search for credentials.
175
+ #
176
+ # @param [Array<String>] new_env_vars
177
+ # @return [Array<String>]
178
+ #
179
+ def self.env_vars= new_env_vars
180
+ new_env_vars = Array new_env_vars unless new_env_vars.nil?
181
+ @env_vars = new_env_vars
182
+ end
183
+
184
+ ##
185
+ # The file paths to search for credentials files.
186
+ #
187
+ # @return [Array<String>]
188
+ #
189
+ def self.paths
190
+ return @paths unless @paths.nil?
191
+
192
+ tmp_paths = []
193
+ # Pull in values is the DEFAULT_PATHS constant exists.
194
+ tmp_paths << const_get(:DEFAULT_PATHS) if const_defined? :DEFAULT_PATHS
195
+ tmp_paths.flatten.uniq
196
+ end
197
+
198
+ ##
199
+ # Set the file paths to search for credentials files.
200
+ #
201
+ # @param [Array<String>] new_paths
202
+ # @return [Array<String>]
203
+ #
204
+ def self.paths= new_paths
205
+ new_paths = Array new_paths unless new_paths.nil?
206
+ @paths = new_paths
207
+ end
208
+
209
+ ##
210
+ # The Signet::OAuth2::Client object the Credentials instance is using.
211
+ #
212
+ # @return [Signet::OAuth2::Client]
213
+ #
48
214
  attr_accessor :client
49
- attr_reader :project_id
50
215
 
51
- # Delegate client methods to the client object.
216
+ ##
217
+ # Identifier for the project the client is authenticating with.
218
+ #
219
+ # @return [String]
220
+ #
221
+ attr_reader :project_id
222
+
223
+ ##
224
+ # Identifier for a separate project used for billing/quota, if any.
225
+ #
226
+ # @return [String,nil]
227
+ #
228
+ attr_reader :quota_project_id
229
+
230
+ # @private Delegate client methods to the client object.
52
231
  extend Forwardable
232
+
233
+ ##
234
+ # @!attribute [r] token_credential_uri
235
+ # @return [String] The token credential URI. The URI is the authorization server's HTTP
236
+ # endpoint capable of issuing tokens and refreshing expired tokens.
237
+ #
238
+ # @!attribute [r] audience
239
+ # @return [String] The target audience ID when issuing assertions. Used only by the
240
+ # assertion grant type.
241
+ #
242
+ # @!attribute [r] scope
243
+ # @return [String, Array<String>] The scope for this client. A scope is an access range
244
+ # defined by the authorization server. The scope can be a single value or a list of values.
245
+ #
246
+ # @!attribute [r] target_audience
247
+ # @return [String] The final target audience for ID tokens returned by this credential.
248
+ #
249
+ # @!attribute [r] issuer
250
+ # @return [String] The issuer ID associated with this client.
251
+ #
252
+ # @!attribute [r] signing_key
253
+ # @return [String, OpenSSL::PKey] The signing key associated with this client.
254
+ #
255
+ # @!attribute [r] updater_proc
256
+ # @return [Proc] Returns a reference to the {Signet::OAuth2::Client#apply} method,
257
+ # suitable for passing as a closure.
258
+ #
53
259
  def_delegators :@client,
54
260
  :token_credential_uri, :audience,
55
- :scope, :issuer, :signing_key, :updater_proc
261
+ :scope, :issuer, :signing_key, :updater_proc, :target_audience
56
262
 
57
- # rubocop:disable Metrics/AbcSize
263
+ ##
264
+ # Creates a new Credentials instance with the provided auth credentials, and with the default
265
+ # values configured on the class.
266
+ #
267
+ # @param [String, Hash, Signet::OAuth2::Client] keyfile
268
+ # The keyfile can be provided as one of the following:
269
+ #
270
+ # * The path to a JSON keyfile (as a +String+)
271
+ # * The contents of a JSON keyfile (as a +Hash+)
272
+ # * A +Signet::OAuth2::Client+ object
273
+ # @param [Hash] options
274
+ # The options for configuring the credentials instance. The following is supported:
275
+ #
276
+ # * +:scope+ - the scope for the client
277
+ # * +"project_id"+ (and optionally +"project"+) - the project identifier for the client
278
+ # * +:connection_builder+ - the connection builder to use for the client
279
+ # * +:default_connection+ - the default connection to use for the client
280
+ #
58
281
  def initialize keyfile, options = {}
59
- scope = options[:scope]
60
282
  verify_keyfile_provided! keyfile
61
283
  @project_id = options["project_id"] || options["project"]
284
+ @quota_project_id = options["quota_project_id"]
62
285
  if keyfile.is_a? Signet::OAuth2::Client
63
- @client = keyfile
64
- @project_id ||= keyfile.project_id if keyfile.respond_to? :project_id
286
+ update_from_signet keyfile
65
287
  elsif keyfile.is_a? Hash
66
- hash = stringify_hash_keys keyfile
67
- hash["scope"] ||= scope
68
- @client = init_client hash, options
69
- @project_id ||= (hash["project_id"] || hash["project"])
288
+ update_from_hash keyfile, options
70
289
  else
71
- verify_keyfile_exists! keyfile
72
- json = JSON.parse ::File.read(keyfile)
73
- json["scope"] ||= scope
74
- @project_id ||= (json["project_id"] || json["project"])
75
- @client = init_client json, options
290
+ update_from_filepath keyfile, options
76
291
  end
77
292
  CredentialsLoader.warn_if_cloud_sdk_credentials @client.client_id
78
293
  @project_id ||= CredentialsLoader.load_gcloud_project_id
79
294
  @client.fetch_access_token!
295
+ @env_vars = nil
296
+ @paths = nil
297
+ @scope = nil
80
298
  end
81
- # rubocop:enable Metrics/AbcSize
82
299
 
83
- # Returns the default credentials checking, in this order, the path env
84
- # evironment variables, json environment variables, default paths. If the
85
- # previously stated locations do not contain keyfile information,
86
- # this method defaults to use the application default.
300
+ ##
301
+ # Creates a new Credentials instance with auth credentials acquired by searching the
302
+ # environment variables and paths configured on the class, and with the default values
303
+ # configured on the class.
304
+ #
305
+ # The auth credentials are searched for in the following order:
306
+ #
307
+ # 1. configured environment variables (see {Credentials.env_vars})
308
+ # 2. configured default file paths (see {Credentials.paths})
309
+ # 3. application default (see {Google::Auth.get_application_default})
310
+ #
311
+ # @param [Hash] options
312
+ # The options for configuring the credentials instance. The following is supported:
313
+ #
314
+ # * +:scope+ - the scope for the client
315
+ # * +"project_id"+ (and optionally +"project"+) - the project identifier for the client
316
+ # * +:connection_builder+ - the connection builder to use for the client
317
+ # * +:default_connection+ - the default connection to use for the client
318
+ #
319
+ # @return [Credentials]
320
+ #
87
321
  def self.default options = {}
88
- # First try to find keyfile file from environment variables.
89
- client = from_path_vars options
90
-
91
- # Second try to find keyfile json from environment variables.
92
- client ||= from_json_vars options
322
+ # First try to find keyfile file or json from environment variables.
323
+ client = from_env_vars options
93
324
 
94
- # Third try to find keyfile file from known file paths.
325
+ # Second try to find keyfile file from known file paths.
95
326
  client ||= from_default_paths options
96
327
 
97
328
  # Finally get instantiated client from Google::Auth
@@ -99,33 +330,22 @@ module Google
99
330
  client
100
331
  end
101
332
 
102
- def self.from_path_vars options
103
- self::PATH_ENV_VARS
104
- .map { |v| ENV[v] }
105
- .compact
106
- .select { |p| ::File.file? p }
107
- .each do |file|
108
- return new file, options
109
- end
110
- nil
111
- end
112
-
113
- def self.from_json_vars options
114
- json = lambda do |v|
115
- unless ENV[v].nil?
116
- begin
117
- JSON.parse ENV[v]
118
- rescue StandardError
119
- nil
120
- end
121
- end
333
+ ##
334
+ # @private Lookup Credentials from environment variables.
335
+ def self.from_env_vars options
336
+ env_vars.each do |env_var|
337
+ str = ENV[env_var]
338
+ next if str.nil?
339
+ return new str, options if ::File.file? str
340
+ return new ::JSON.parse(str), options rescue nil
122
341
  end
123
- self::JSON_ENV_VARS.map(&json).compact.each { |hash| return new hash, options }
124
342
  nil
125
343
  end
126
344
 
345
+ ##
346
+ # @private Lookup Credentials from default file paths.
127
347
  def self.from_default_paths options
128
- self::DEFAULT_PATHS
348
+ paths
129
349
  .select { |p| ::File.file? p }
130
350
  .each do |file|
131
351
  return new file, options
@@ -133,13 +353,16 @@ module Google
133
353
  nil
134
354
  end
135
355
 
356
+ ##
357
+ # @private Lookup Credentials using Google::Auth.get_application_default.
136
358
  def self.from_application_default options
137
- scope = options[:scope] || self::SCOPE
138
- client = Google::Auth.get_application_default scope
359
+ scope = options[:scope] || self.scope
360
+ auth_opts = { target_audience: options[:target_audience] || target_audience }
361
+ client = Google::Auth.get_application_default scope, auth_opts
139
362
  new client, options
140
363
  end
141
- private_class_method :from_path_vars,
142
- :from_json_vars,
364
+
365
+ private_class_method :from_env_vars,
143
366
  :from_default_paths,
144
367
  :from_application_default
145
368
 
@@ -171,17 +394,49 @@ module Google
171
394
 
172
395
  def client_options options
173
396
  # Keyfile options have higher priority over constructor defaults
174
- options["token_credential_uri"] ||= self.class::TOKEN_CREDENTIAL_URI
175
- options["audience"] ||= self.class::AUDIENCE
176
- options["scope"] ||= self.class::SCOPE
397
+ options["token_credential_uri"] ||= self.class.token_credential_uri
398
+ options["audience"] ||= self.class.audience
399
+ options["scope"] ||= self.class.scope
400
+ options["target_audience"] ||= self.class.target_audience
401
+
402
+ if !Array(options["scope"]).empty? && options["target_audience"]
403
+ raise ArgumentError, "Cannot specify both scope and target_audience"
404
+ end
177
405
 
406
+ needs_scope = options["target_audience"].nil?
178
407
  # client options for initializing signet client
179
408
  { token_credential_uri: options["token_credential_uri"],
180
409
  audience: options["audience"],
181
- scope: Array(options["scope"]),
410
+ scope: (needs_scope ? Array(options["scope"]) : nil),
411
+ target_audience: options["target_audience"],
182
412
  issuer: options["client_email"],
183
413
  signing_key: OpenSSL::PKey::RSA.new(options["private_key"]) }
184
414
  end
415
+
416
+ def update_from_signet client
417
+ @project_id ||= client.project_id if client.respond_to? :project_id
418
+ @quota_project_id ||= client.quota_project_id if client.respond_to? :quota_project_id
419
+ @client = client
420
+ end
421
+
422
+ def update_from_hash hash, options
423
+ hash = stringify_hash_keys hash
424
+ hash["scope"] ||= options[:scope]
425
+ hash["target_audience"] ||= options[:target_audience]
426
+ @project_id ||= (hash["project_id"] || hash["project"])
427
+ @quota_project_id ||= hash["quota_project_id"]
428
+ @client = init_client hash, options
429
+ end
430
+
431
+ def update_from_filepath path, options
432
+ verify_keyfile_exists! path
433
+ json = JSON.parse ::File.read(path)
434
+ json["scope"] ||= options[:scope]
435
+ json["target_audience"] ||= options[:target_audience]
436
+ @project_id ||= (json["project_id"] || json["project"])
437
+ @quota_project_id ||= json["quota_project_id"]
438
+ @client = init_client json, options
439
+ end
185
440
  end
186
441
  end
187
442
  end