googleauth 0.5.1 → 0.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.
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