googleauth 0.5.1 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. checksums.yaml +5 -5
  2. data/.github/CODEOWNERS +7 -0
  3. data/{CONTRIBUTING.md → .github/CONTRIBUTING.md} +5 -4
  4. data/.github/ISSUE_TEMPLATE/bug_report.md +36 -0
  5. data/.github/ISSUE_TEMPLATE/feature_request.md +21 -0
  6. data/.github/ISSUE_TEMPLATE/support_request.md +7 -0
  7. data/.kokoro/build.bat +16 -0
  8. data/.kokoro/build.sh +4 -0
  9. data/.kokoro/continuous/common.cfg +24 -0
  10. data/.kokoro/continuous/linux.cfg +25 -0
  11. data/.kokoro/continuous/osx.cfg +8 -0
  12. data/.kokoro/continuous/post.cfg +30 -0
  13. data/.kokoro/continuous/windows.cfg +29 -0
  14. data/.kokoro/osx.sh +4 -0
  15. data/.kokoro/presubmit/common.cfg +24 -0
  16. data/.kokoro/presubmit/linux.cfg +24 -0
  17. data/.kokoro/presubmit/osx.cfg +8 -0
  18. data/.kokoro/presubmit/windows.cfg +29 -0
  19. data/.kokoro/release.cfg +94 -0
  20. data/.kokoro/trampoline.bat +10 -0
  21. data/.kokoro/trampoline.sh +4 -0
  22. data/.repo-metadata.json +5 -0
  23. data/.rubocop.yml +19 -1
  24. data/CHANGELOG.md +112 -19
  25. data/CODE_OF_CONDUCT.md +43 -0
  26. data/Gemfile +19 -13
  27. data/{COPYING → LICENSE} +0 -0
  28. data/README.md +58 -18
  29. data/Rakefile +126 -9
  30. data/googleauth.gemspec +28 -25
  31. data/integration/helper.rb +31 -0
  32. data/integration/id_tokens/key_source_test.rb +74 -0
  33. data/lib/googleauth.rb +7 -96
  34. data/lib/googleauth/application_default.rb +81 -0
  35. data/lib/googleauth/client_id.rb +21 -19
  36. data/lib/googleauth/compute_engine.rb +70 -43
  37. data/lib/googleauth/credentials.rb +442 -0
  38. data/lib/googleauth/credentials_loader.rb +117 -43
  39. data/lib/googleauth/default_credentials.rb +93 -0
  40. data/lib/googleauth/iam.rb +11 -11
  41. data/lib/googleauth/id_tokens.rb +233 -0
  42. data/lib/googleauth/id_tokens/errors.rb +71 -0
  43. data/lib/googleauth/id_tokens/key_sources.rb +394 -0
  44. data/lib/googleauth/id_tokens/verifier.rb +144 -0
  45. data/lib/googleauth/json_key_reader.rb +50 -0
  46. data/lib/googleauth/scope_util.rb +12 -12
  47. data/lib/googleauth/service_account.rb +74 -63
  48. data/lib/googleauth/signet.rb +55 -13
  49. data/lib/googleauth/stores/file_token_store.rb +8 -8
  50. data/lib/googleauth/stores/redis_token_store.rb +22 -22
  51. data/lib/googleauth/token_store.rb +6 -6
  52. data/lib/googleauth/user_authorizer.rb +80 -68
  53. data/lib/googleauth/user_refresh.rb +44 -35
  54. data/lib/googleauth/version.rb +1 -1
  55. data/lib/googleauth/web_user_authorizer.rb +77 -68
  56. data/rakelib/devsite_builder.rb +45 -0
  57. data/rakelib/link_checker.rb +64 -0
  58. data/rakelib/repo_metadata.rb +59 -0
  59. data/spec/googleauth/apply_auth_examples.rb +74 -50
  60. data/spec/googleauth/client_id_spec.rb +75 -55
  61. data/spec/googleauth/compute_engine_spec.rb +98 -46
  62. data/spec/googleauth/credentials_spec.rb +478 -0
  63. data/spec/googleauth/get_application_default_spec.rb +149 -111
  64. data/spec/googleauth/iam_spec.rb +25 -25
  65. data/spec/googleauth/scope_util_spec.rb +26 -24
  66. data/spec/googleauth/service_account_spec.rb +269 -144
  67. data/spec/googleauth/signet_spec.rb +101 -30
  68. data/spec/googleauth/stores/file_token_store_spec.rb +12 -13
  69. data/spec/googleauth/stores/redis_token_store_spec.rb +11 -11
  70. data/spec/googleauth/stores/store_examples.rb +16 -16
  71. data/spec/googleauth/user_authorizer_spec.rb +153 -124
  72. data/spec/googleauth/user_refresh_spec.rb +186 -121
  73. data/spec/googleauth/web_user_authorizer_spec.rb +82 -69
  74. data/spec/spec_helper.rb +21 -19
  75. data/test/helper.rb +33 -0
  76. data/test/id_tokens/key_sources_test.rb +240 -0
  77. data/test/id_tokens/verifier_test.rb +269 -0
  78. metadata +87 -34
  79. data/.rubocop_todo.yml +0 -32
  80. data/.travis.yml +0 -37
@@ -0,0 +1,442 @@
1
+ # Copyright 2017, Google Inc.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are
6
+ # met:
7
+ #
8
+ # * Redistributions of source code must retain the above copyright
9
+ # notice, this list of conditions and the following disclaimer.
10
+ # * Redistributions in binary form must reproduce the above
11
+ # copyright notice, this list of conditions and the following disclaimer
12
+ # in the documentation and/or other materials provided with the
13
+ # distribution.
14
+ # * Neither the name of Google Inc. nor the names of its
15
+ # contributors may be used to endorse or promote products derived from
16
+ # this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
+
30
+ require "forwardable"
31
+ require "json"
32
+ require "signet/oauth_2/client"
33
+
34
+ require "googleauth/credentials_loader"
35
+
36
+ module Google
37
+ module Auth
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.
41
+ class Credentials
42
+ ##
43
+ # The default token credential URI to be used when none is provided during initialization.
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.
48
+ AUDIENCE = "https://oauth2.googleapis.com/token".freeze
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
+ #
214
+ attr_accessor :client
215
+
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.
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
+ #
259
+ def_delegators :@client,
260
+ :token_credential_uri, :audience,
261
+ :scope, :issuer, :signing_key, :updater_proc, :target_audience
262
+
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
+ #
281
+ def initialize keyfile, options = {}
282
+ verify_keyfile_provided! keyfile
283
+ @project_id = options["project_id"] || options["project"]
284
+ @quota_project_id = options["quota_project_id"]
285
+ if keyfile.is_a? Signet::OAuth2::Client
286
+ update_from_signet keyfile
287
+ elsif keyfile.is_a? Hash
288
+ update_from_hash keyfile, options
289
+ else
290
+ update_from_filepath keyfile, options
291
+ end
292
+ CredentialsLoader.warn_if_cloud_sdk_credentials @client.client_id
293
+ @project_id ||= CredentialsLoader.load_gcloud_project_id
294
+ @client.fetch_access_token!
295
+ @env_vars = nil
296
+ @paths = nil
297
+ @scope = nil
298
+ end
299
+
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
+ #
321
+ def self.default options = {}
322
+ # First try to find keyfile file or json from environment variables.
323
+ client = from_env_vars options
324
+
325
+ # Second try to find keyfile file from known file paths.
326
+ client ||= from_default_paths options
327
+
328
+ # Finally get instantiated client from Google::Auth
329
+ client ||= from_application_default options
330
+ client
331
+ end
332
+
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
341
+ end
342
+ nil
343
+ end
344
+
345
+ ##
346
+ # @private Lookup Credentials from default file paths.
347
+ def self.from_default_paths options
348
+ paths
349
+ .select { |p| ::File.file? p }
350
+ .each do |file|
351
+ return new file, options
352
+ end
353
+ nil
354
+ end
355
+
356
+ ##
357
+ # @private Lookup Credentials using Google::Auth.get_application_default.
358
+ def self.from_application_default options
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
362
+ new client, options
363
+ end
364
+
365
+ private_class_method :from_env_vars,
366
+ :from_default_paths,
367
+ :from_application_default
368
+
369
+ protected
370
+
371
+ # Verify that the keyfile argument is provided.
372
+ def verify_keyfile_provided! keyfile
373
+ return unless keyfile.nil?
374
+ raise "The keyfile passed to Google::Auth::Credentials.new was nil."
375
+ end
376
+
377
+ # Verify that the keyfile argument is a file.
378
+ def verify_keyfile_exists! keyfile
379
+ exists = ::File.file? keyfile
380
+ raise "The keyfile '#{keyfile}' is not a valid file." unless exists
381
+ end
382
+
383
+ # Initializes the Signet client.
384
+ def init_client keyfile, connection_options = {}
385
+ client_opts = client_options keyfile
386
+ Signet::OAuth2::Client.new(client_opts)
387
+ .configure_connection(connection_options)
388
+ end
389
+
390
+ # returns a new Hash with string keys instead of symbol keys.
391
+ def stringify_hash_keys hash
392
+ Hash[hash.map { |k, v| [k.to_s, v] }]
393
+ end
394
+
395
+ def client_options options
396
+ # Keyfile options have higher priority over constructor defaults
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
405
+
406
+ needs_scope = options["target_audience"].nil?
407
+ # client options for initializing signet client
408
+ { token_credential_uri: options["token_credential_uri"],
409
+ audience: options["audience"],
410
+ scope: (needs_scope ? Array(options["scope"]) : nil),
411
+ target_audience: options["target_audience"],
412
+ issuer: options["client_email"],
413
+ signing_key: OpenSSL::PKey::RSA.new(options["private_key"]) }
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
440
+ end
441
+ end
442
+ end
@@ -27,9 +27,9 @@
27
27
  # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
28
  # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
29
 
30
- require 'memoist'
31
- require 'os'
32
- require 'rbconfig'
30
+ require "memoist"
31
+ require "os"
32
+ require "rbconfig"
33
33
 
34
34
  module Google
35
35
  # Module Auth provides classes that provide Google-specific authorization
@@ -39,44 +39,71 @@ module Google
39
39
  # credentials files on the file system.
40
40
  module CredentialsLoader
41
41
  extend Memoist
42
- ENV_VAR = 'GOOGLE_APPLICATION_CREDENTIALS'
42
+ ENV_VAR = "GOOGLE_APPLICATION_CREDENTIALS".freeze
43
+ PRIVATE_KEY_VAR = "GOOGLE_PRIVATE_KEY".freeze
44
+ CLIENT_EMAIL_VAR = "GOOGLE_CLIENT_EMAIL".freeze
45
+ CLIENT_ID_VAR = "GOOGLE_CLIENT_ID".freeze
46
+ CLIENT_SECRET_VAR = "GOOGLE_CLIENT_SECRET".freeze
47
+ REFRESH_TOKEN_VAR = "GOOGLE_REFRESH_TOKEN".freeze
48
+ ACCOUNT_TYPE_VAR = "GOOGLE_ACCOUNT_TYPE".freeze
49
+ PROJECT_ID_VAR = "GOOGLE_PROJECT_ID".freeze
50
+ GCLOUD_POSIX_COMMAND = "gcloud".freeze
51
+ GCLOUD_WINDOWS_COMMAND = "gcloud.cmd".freeze
52
+ GCLOUD_CONFIG_COMMAND =
53
+ "config config-helper --format json --verbosity none".freeze
43
54
 
44
- PRIVATE_KEY_VAR = 'GOOGLE_PRIVATE_KEY'
45
- CLIENT_EMAIL_VAR = 'GOOGLE_CLIENT_EMAIL'
46
- CLIENT_ID_VAR = 'GOOGLE_CLIENT_ID'
47
- CLIENT_SECRET_VAR = 'GOOGLE_CLIENT_SECRET'
48
- REFRESH_TOKEN_VAR = 'GOOGLE_REFRESH_TOKEN'
49
- ACCOUNT_TYPE_VAR = 'GOOGLE_ACCOUNT_TYPE'
50
-
51
- CREDENTIALS_FILE_NAME = 'application_default_credentials.json'
55
+ CREDENTIALS_FILE_NAME = "application_default_credentials.json".freeze
52
56
  NOT_FOUND_ERROR =
53
- "Unable to read the credential file specified by #{ENV_VAR}"
54
- WELL_KNOWN_PATH = "gcloud/#{CREDENTIALS_FILE_NAME}"
55
- WELL_KNOWN_ERROR = 'Unable to read the default credential file'
57
+ "Unable to read the credential file specified by #{ENV_VAR}".freeze
58
+ WELL_KNOWN_PATH = "gcloud/#{CREDENTIALS_FILE_NAME}".freeze
59
+ WELL_KNOWN_ERROR = "Unable to read the default credential file".freeze
60
+
61
+ SYSTEM_DEFAULT_ERROR =
62
+ "Unable to read the system default credential file".freeze
63
+
64
+ CLOUD_SDK_CLIENT_ID = "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.app"\
65
+ "s.googleusercontent.com".freeze
56
66
 
57
- SYSTEM_DEFAULT_ERROR = 'Unable to read the system default credential file'
67
+ CLOUD_SDK_CREDENTIALS_WARNING = "Your application has authenticated using end user "\
68
+ "credentials from Google Cloud SDK. We recommend that most server applications use "\
69
+ "service accounts instead. If your application continues to use end user credentials "\
70
+ 'from Cloud SDK, you might receive a "quota exceeded" or "API not enabled" error. For '\
71
+ "more information about service accounts, see "\
72
+ "https://cloud.google.com/docs/authentication/. To suppress this message, set the "\
73
+ "GOOGLE_AUTH_SUPPRESS_CREDENTIALS_WARNINGS environment variable.".freeze
58
74
 
59
75
  # make_creds proxies the construction of a credentials instance
60
76
  #
61
77
  # By default, it calls #new on the current class, but this behaviour can
62
78
  # be modified, allowing different instances to be created.
63
- def make_creds(*args)
64
- new(*args)
79
+ def make_creds *args
80
+ creds = new(*args)
81
+ creds = creds.configure_connection args[0] if creds.respond_to?(:configure_connection) && args.size == 1
82
+ creds
65
83
  end
66
84
 
67
85
  # Creates an instance from the path specified in an environment
68
86
  # variable.
69
87
  #
70
88
  # @param scope [string|array|nil] the scope(s) to access
71
- def from_env(scope = nil)
72
- if ENV.key?(ENV_VAR)
89
+ # @param options [Hash] Connection options. These may be used to configure
90
+ # how OAuth tokens are retrieved, by providing a suitable
91
+ # `Faraday::Connection`. For example, if a connection proxy must be
92
+ # used in the current network, you may provide a connection with
93
+ # with the needed proxy options.
94
+ # The following keys are recognized:
95
+ # * `:default_connection` The connection object to use.
96
+ # * `:connection_builder` A `Proc` that returns a connection.
97
+ def from_env scope = nil, options = {}
98
+ options = interpret_options scope, options
99
+ if ENV.key?(ENV_VAR) && !ENV[ENV_VAR].empty?
73
100
  path = ENV[ENV_VAR]
74
- fail "file #{path} does not exist" unless File.exist?(path)
75
- File.open(path) do |f|
76
- return make_creds(json_key_io: f, scope: scope)
101
+ raise "file #{path} does not exist" unless File.exist? path
102
+ File.open path do |f|
103
+ return make_creds options.merge(json_key_io: f)
77
104
  end
78
105
  elsif service_account_env_vars? || authorized_user_env_vars?
79
- return make_creds(scope: scope)
106
+ return make_creds options
80
107
  end
81
108
  rescue StandardError => e
82
109
  raise "#{NOT_FOUND_ERROR}: #{e}"
@@ -85,15 +112,24 @@ module Google
85
112
  # Creates an instance from a well known path.
86
113
  #
87
114
  # @param scope [string|array|nil] the scope(s) to access
88
- def from_well_known_path(scope = nil)
89
- home_var = OS.windows? ? 'APPDATA' : 'HOME'
115
+ # @param options [Hash] Connection options. These may be used to configure
116
+ # how OAuth tokens are retrieved, by providing a suitable
117
+ # `Faraday::Connection`. For example, if a connection proxy must be
118
+ # used in the current network, you may provide a connection with
119
+ # with the needed proxy options.
120
+ # The following keys are recognized:
121
+ # * `:default_connection` The connection object to use.
122
+ # * `:connection_builder` A `Proc` that returns a connection.
123
+ def from_well_known_path scope = nil, options = {}
124
+ options = interpret_options scope, options
125
+ home_var = OS.windows? ? "APPDATA" : "HOME"
90
126
  base = WELL_KNOWN_PATH
91
- root = ENV[home_var].nil? ? '' : ENV[home_var]
92
- base = File.join('.config', base) unless OS.windows?
93
- path = File.join(root, base)
94
- return nil unless File.exist?(path)
95
- File.open(path) do |f|
96
- return make_creds(json_key_io: f, scope: scope)
127
+ root = ENV[home_var].nil? ? "" : ENV[home_var]
128
+ base = File.join ".config", base unless OS.windows?
129
+ path = File.join root, base
130
+ return nil unless File.exist? path
131
+ File.open path do |f|
132
+ return make_creds options.merge(json_key_io: f)
97
133
  end
98
134
  rescue StandardError => e
99
135
  raise "#{WELL_KNOWN_ERROR}: #{e}"
@@ -102,31 +138,69 @@ module Google
102
138
  # Creates an instance from the system default path
103
139
  #
104
140
  # @param scope [string|array|nil] the scope(s) to access
105
- def from_system_default_path(scope = nil)
141
+ # @param options [Hash] Connection options. These may be used to configure
142
+ # how OAuth tokens are retrieved, by providing a suitable
143
+ # `Faraday::Connection`. For example, if a connection proxy must be
144
+ # used in the current network, you may provide a connection with
145
+ # with the needed proxy options.
146
+ # The following keys are recognized:
147
+ # * `:default_connection` The connection object to use.
148
+ # * `:connection_builder` A `Proc` that returns a connection.
149
+ def from_system_default_path scope = nil, options = {}
150
+ options = interpret_options scope, options
106
151
  if OS.windows?
107
- return nil unless ENV['ProgramData']
108
- prefix = File.join(ENV['ProgramData'], 'Google/Auth')
152
+ return nil unless ENV["ProgramData"]
153
+ prefix = File.join ENV["ProgramData"], "Google/Auth"
109
154
  else
110
- prefix = '/etc/google/auth/'
155
+ prefix = "/etc/google/auth/"
111
156
  end
112
- path = File.join(prefix, CREDENTIALS_FILE_NAME)
113
- return nil unless File.exist?(path)
114
- File.open(path) do |f|
115
- return make_creds(json_key_io: f, scope: scope)
157
+ path = File.join prefix, CREDENTIALS_FILE_NAME
158
+ return nil unless File.exist? path
159
+ File.open path do |f|
160
+ return make_creds options.merge(json_key_io: f)
116
161
  end
117
162
  rescue StandardError => e
118
163
  raise "#{SYSTEM_DEFAULT_ERROR}: #{e}"
119
164
  end
120
165
 
166
+ module_function
167
+
168
+ # Issues warning if cloud sdk client id is used
169
+ def warn_if_cloud_sdk_credentials client_id
170
+ return if ENV["GOOGLE_AUTH_SUPPRESS_CREDENTIALS_WARNINGS"]
171
+ warn CLOUD_SDK_CREDENTIALS_WARNING if client_id == CLOUD_SDK_CLIENT_ID
172
+ end
173
+
174
+ # Finds project_id from gcloud CLI configuration
175
+ def load_gcloud_project_id
176
+ gcloud = GCLOUD_WINDOWS_COMMAND if OS.windows?
177
+ gcloud = GCLOUD_POSIX_COMMAND unless OS.windows?
178
+ gcloud_json = IO.popen("#{gcloud} #{GCLOUD_CONFIG_COMMAND}", &:read)
179
+ config = MultiJson.load gcloud_json
180
+ config["configuration"]["properties"]["core"]["project"]
181
+ rescue StandardError
182
+ nil
183
+ end
184
+
121
185
  private
122
186
 
187
+ def interpret_options scope, options
188
+ if scope.is_a? Hash
189
+ options = scope
190
+ scope = nil
191
+ end
192
+ return options.merge scope: scope if scope && !options[:scope]
193
+ options
194
+ end
195
+
123
196
  def service_account_env_vars?
124
- ([PRIVATE_KEY_VAR, CLIENT_EMAIL_VAR] - ENV.keys).empty?
197
+ ([PRIVATE_KEY_VAR, CLIENT_EMAIL_VAR] - ENV.keys).empty? &&
198
+ !ENV.to_h.fetch_values(PRIVATE_KEY_VAR, CLIENT_EMAIL_VAR).join(" ").empty?
125
199
  end
126
200
 
127
201
  def authorized_user_env_vars?
128
- ([CLIENT_ID_VAR, CLIENT_SECRET_VAR, REFRESH_TOKEN_VAR] -
129
- ENV.keys).empty?
202
+ ([CLIENT_ID_VAR, CLIENT_SECRET_VAR, REFRESH_TOKEN_VAR] - ENV.keys).empty? &&
203
+ !ENV.to_h.fetch_values(CLIENT_ID_VAR, CLIENT_SECRET_VAR, REFRESH_TOKEN_VAR).join(" ").empty?
130
204
  end
131
205
  end
132
206
  end