File: credentials.rb

package info (click to toggle)
ruby-googleauth 1.3.0-4
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 284 kB
  • sloc: ruby: 1,517; makefile: 4
file content (546 lines) | stat: -rw-r--r-- 20,373 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
# Copyright 2017 Google, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

require "forwardable"
require "json"
require "signet/oauth_2/client"

require "googleauth/credentials_loader"

module Google
  module Auth
    ##
    # Credentials is a high-level base class used by Google's API client
    # libraries to represent the authentication when connecting to an API.
    # In most cases, it is subclassed by API-specific credential classes that
    # can be instantiated by clients.
    #
    # ## Options
    #
    # Credentials classes are configured with options that dictate default
    # values for parameters such as scope and audience. These defaults are
    # expressed as class attributes, and may differ from endpoint to endpoint.
    # Normally, an API client will provide subclasses specific to each
    # endpoint, configured with appropriate values.
    #
    # Note that these options inherit up the class hierarchy. If a particular
    # options is not set for a subclass, its superclass is queried.
    #
    # Some older users of this class set options via constants. This usage is
    # deprecated. For example, instead of setting the `AUDIENCE` constant on
    # your subclass, call the `audience=` method.
    #
    # ## Example
    #
    #     class MyCredentials < Google::Auth::Credentials
    #       # Set the default scope for these credentials
    #       self.scope = "http://example.com/my_scope"
    #     end
    #
    #     # creds is a credentials object suitable for Google API clients
    #     creds = MyCredentials.default
    #     creds.scope  # => ["http://example.com/my_scope"]
    #
    #     class SubCredentials < MyCredentials
    #       # Override the default scope for this subclass
    #       self.scope = "http://example.com/sub_scope"
    #     end
    #
    #     creds2 = SubCredentials.default
    #     creds2.scope  # => ["http://example.com/sub_scope"]
    #
    class Credentials # rubocop:disable Metrics/ClassLength
      ##
      # The default token credential URI to be used when none is provided during initialization.
      TOKEN_CREDENTIAL_URI = "https://oauth2.googleapis.com/token".freeze

      ##
      # The default target audience ID to be used when none is provided during initialization.
      AUDIENCE = "https://oauth2.googleapis.com/token".freeze

      @audience = @scope = @target_audience = @env_vars = @paths = @token_credential_uri = nil

      ##
      # The default token credential URI to be used when none is provided during initialization.
      # The URI is the authorization server's HTTP endpoint capable of issuing tokens and
      # refreshing expired tokens.
      #
      # @return [String]
      #
      def self.token_credential_uri
        lookup_auth_param :token_credential_uri do
          lookup_local_constant :TOKEN_CREDENTIAL_URI
        end
      end

      ##
      # Set the default token credential URI to be used when none is provided during initialization.
      #
      # @param [String] new_token_credential_uri
      #
      def self.token_credential_uri= new_token_credential_uri
        @token_credential_uri = new_token_credential_uri
      end

      ##
      # The default target audience ID to be used when none is provided during initialization.
      # Used only by the assertion grant type.
      #
      # @return [String]
      #
      def self.audience
        lookup_auth_param :audience do
          lookup_local_constant :AUDIENCE
        end
      end

      ##
      # Sets the default target audience ID to be used when none is provided during initialization.
      #
      # @param [String] new_audience
      #
      def self.audience= new_audience
        @audience = new_audience
      end

      ##
      # The default scope to be used when none is provided during initialization.
      # A scope is an access range defined by the authorization server.
      # The scope can be a single value or a list of values.
      #
      # Either {#scope} or {#target_audience}, but not both, should be non-nil.
      # If {#scope} is set, this credential will produce access tokens.
      # If {#target_audience} is set, this credential will produce ID tokens.
      #
      # @return [String, Array<String>, nil]
      #
      def self.scope
        lookup_auth_param :scope do
          vals = lookup_local_constant :SCOPE
          vals ? Array(vals).flatten.uniq : nil
        end
      end

      ##
      # Sets the default scope to be used when none is provided during initialization.
      #
      # Either {#scope} or {#target_audience}, but not both, should be non-nil.
      # If {#scope} is set, this credential will produce access tokens.
      # If {#target_audience} is set, this credential will produce ID tokens.
      #
      # @param [String, Array<String>, nil] new_scope
      #
      def self.scope= new_scope
        new_scope = Array new_scope unless new_scope.nil?
        @scope = new_scope
      end

      ##
      # The default final target audience for ID tokens, to be used when none
      # is provided during initialization.
      #
      # Either {#scope} or {#target_audience}, but not both, should be non-nil.
      # If {#scope} is set, this credential will produce access tokens.
      # If {#target_audience} is set, this credential will produce ID tokens.
      #
      # @return [String, nil]
      #
      def self.target_audience
        lookup_auth_param :target_audience
      end

      ##
      # Sets the default final target audience for ID tokens, to be used when none
      # is provided during initialization.
      #
      # Either {#scope} or {#target_audience}, but not both, should be non-nil.
      # If {#scope} is set, this credential will produce access tokens.
      # If {#target_audience} is set, this credential will produce ID tokens.
      #
      # @param [String, nil] new_target_audience
      #
      def self.target_audience= new_target_audience
        @target_audience = new_target_audience
      end

      ##
      # The environment variables to search for credentials. Values can either be a file path to the
      # credentials file, or the JSON contents of the credentials file.
      # The env_vars will never be nil. If there are no vars, the empty array is returned.
      #
      # @return [Array<String>]
      #
      def self.env_vars
        env_vars_internal || []
      end

      ##
      # @private
      # Internal recursive lookup for env_vars.
      #
      def self.env_vars_internal
        lookup_auth_param :env_vars, :env_vars_internal do
          # Pull values when PATH_ENV_VARS or JSON_ENV_VARS constants exists.
          path_env_vars = lookup_local_constant :PATH_ENV_VARS
          json_env_vars = lookup_local_constant :JSON_ENV_VARS
          (Array(path_env_vars) + Array(json_env_vars)).flatten.uniq if path_env_vars || json_env_vars
        end
      end

      ##
      # Sets the environment variables to search for credentials.
      # Setting to `nil` "unsets" the value, and defaults to the superclass
      # (or to the empty array if there is no superclass).
      #
      # @param [String, Array<String>, nil] new_env_vars
      #
      def self.env_vars= new_env_vars
        new_env_vars = Array new_env_vars unless new_env_vars.nil?
        @env_vars = new_env_vars
      end

      ##
      # The file paths to search for credentials files.
      # The paths will never be nil. If there are no paths, the empty array is returned.
      #
      # @return [Array<String>]
      #
      def self.paths
        paths_internal || []
      end

      ##
      # @private
      # Internal recursive lookup for paths.
      #
      def self.paths_internal
        lookup_auth_param :paths, :paths_internal do
          # Pull in values if the DEFAULT_PATHS constant exists.
          vals = lookup_local_constant :DEFAULT_PATHS
          vals ? Array(vals).flatten.uniq : nil
        end
      end

      ##
      # Set the file paths to search for credentials files.
      # Setting to `nil` "unsets" the value, and defaults to the superclass
      # (or to the empty array if there is no superclass).
      #
      # @param [String, Array<String>, nil] new_paths
      #
      def self.paths= new_paths
        new_paths = Array new_paths unless new_paths.nil?
        @paths = new_paths
      end

      ##
      # @private
      # Return the given parameter value, defaulting up the class hierarchy.
      #
      # First returns the value of the instance variable, if set.
      # Next, calls the given block if provided. (This is generally used to
      # look up legacy constant-based values.)
      # Otherwise, calls the superclass method if present.
      # Returns nil if all steps fail.
      #
      # @param name [Symbol] The parameter name
      # @param method_name [Symbol] The lookup method name, if different
      # @return [Object] The value
      #
      def self.lookup_auth_param name, method_name = name
        val = instance_variable_get "@#{name}".to_sym
        val = yield if val.nil? && block_given?
        return val unless val.nil?
        return superclass.send method_name if superclass.respond_to? method_name
        nil
      end

      ##
      # @private
      # Return the value of the given constant if it is defined directly in
      # this class, or nil if not.
      #
      # @param [Symbol] Name of the constant
      # @return [Object] The value
      #
      def self.lookup_local_constant name
        const_defined?(name, false) ? const_get(name) : nil
      end

      ##
      # The Signet::OAuth2::Client object the Credentials instance is using.
      #
      # @return [Signet::OAuth2::Client]
      #
      attr_accessor :client

      ##
      # Identifier for the project the client is authenticating with.
      #
      # @return [String]
      #
      attr_reader :project_id

      ##
      # Identifier for a separate project used for billing/quota, if any.
      #
      # @return [String,nil]
      #
      attr_reader :quota_project_id

      # @private Delegate client methods to the client object.
      extend Forwardable

      ##
      # @!attribute [r] token_credential_uri
      #   @return [String] The token credential URI. The URI is the authorization server's HTTP
      #     endpoint capable of issuing tokens and refreshing expired tokens.
      #
      # @!attribute [r] audience
      #   @return [String] The target audience ID when issuing assertions. Used only by the
      #     assertion grant type.
      #
      # @!attribute [r] scope
      #   @return [String, Array<String>] The scope for this client. A scope is an access range
      #     defined by the authorization server. The scope can be a single value or a list of values.
      #
      # @!attribute [r] target_audience
      #   @return [String] The final target audience for ID tokens returned by this credential.
      #
      # @!attribute [r] issuer
      #   @return [String] The issuer ID associated with this client.
      #
      # @!attribute [r] signing_key
      #   @return [String, OpenSSL::PKey] The signing key associated with this client.
      #
      # @!attribute [r] updater_proc
      #   @return [Proc] Returns a reference to the {Signet::OAuth2::Client#apply} method,
      #     suitable for passing as a closure.
      #
      def_delegators :@client,
                     :token_credential_uri, :audience,
                     :scope, :issuer, :signing_key, :updater_proc, :target_audience

      ##
      # Creates a new Credentials instance with the provided auth credentials, and with the default
      # values configured on the class.
      #
      # @param [String, Hash, Signet::OAuth2::Client] keyfile
      #   The keyfile can be provided as one of the following:
      #
      #   * The path to a JSON keyfile (as a +String+)
      #   * The contents of a JSON keyfile (as a +Hash+)
      #   * A +Signet::OAuth2::Client+ object
      # @param [Hash] options
      #   The options for configuring the credentials instance. The following is supported:
      #
      #   * +:scope+ - the scope for the client
      #   * +"project_id"+ (and optionally +"project"+) - the project identifier for the client
      #   * +:connection_builder+ - the connection builder to use for the client
      #   * +:default_connection+ - the default connection to use for the client
      #
      def initialize keyfile, options = {}
        verify_keyfile_provided! keyfile
        @project_id = options["project_id"] || options["project"]
        @quota_project_id = options["quota_project_id"]
        case keyfile
        when Signet::OAuth2::Client
          update_from_signet keyfile
        when Hash
          update_from_hash keyfile, options
        else
          update_from_filepath keyfile, options
        end
        CredentialsLoader.warn_if_cloud_sdk_credentials @client.client_id
        @project_id ||= CredentialsLoader.load_gcloud_project_id
        @client.fetch_access_token! if @client.needs_access_token?
        @env_vars = nil
        @paths = nil
        @scope = nil
      end

      ##
      # Creates a new Credentials instance with auth credentials acquired by searching the
      # environment variables and paths configured on the class, and with the default values
      # configured on the class.
      #
      # The auth credentials are searched for in the following order:
      #
      # 1. configured environment variables (see {Credentials.env_vars})
      # 2. configured default file paths (see {Credentials.paths})
      # 3. application default (see {Google::Auth.get_application_default})
      #
      # @param [Hash] options
      #   The options for configuring the credentials instance. The following is supported:
      #
      #   * +:scope+ - the scope for the client
      #   * +"project_id"+ (and optionally +"project"+) - the project identifier for the client
      #   * +:connection_builder+ - the connection builder to use for the client
      #   * +:default_connection+ - the default connection to use for the client
      #
      # @return [Credentials]
      #
      def self.default options = {}
        # First try to find keyfile file or json from environment variables.
        client = from_env_vars options

        # Second try to find keyfile file from known file paths.
        client ||= from_default_paths options

        # Finally get instantiated client from Google::Auth
        client ||= from_application_default options
        client
      end

      ##
      # @private Lookup Credentials from environment variables.
      def self.from_env_vars options
        env_vars.each do |env_var|
          str = ENV[env_var]
          next if str.nil?
          io =
            if ::File.file? str
              ::StringIO.new ::File.read str
            else
              json = ::JSON.parse str rescue nil
              json ? ::StringIO.new(str) : nil
            end
          next if io.nil?
          return from_io io, options
        end
        nil
      end

      ##
      # @private Lookup Credentials from default file paths.
      def self.from_default_paths options
        paths.each do |path|
          next unless path && ::File.file?(path)
          io = ::StringIO.new ::File.read path
          return from_io io, options
        end
        nil
      end

      ##
      # @private Lookup Credentials using Google::Auth.get_application_default.
      def self.from_application_default options
        scope = options[:scope] || self.scope
        auth_opts = {
          token_credential_uri:   options[:token_credential_uri] || token_credential_uri,
          audience:               options[:audience] || audience,
          target_audience:        options[:target_audience] || target_audience,
          enable_self_signed_jwt: options[:enable_self_signed_jwt] && options[:scope].nil?
        }
        client = Google::Auth.get_application_default scope, auth_opts
        new client, options
      end

      # @private Read credentials from a JSON stream.
      def self.from_io io, options
        creds_input = {
          json_key_io:            io,
          scope:                  options[:scope] || scope,
          target_audience:        options[:target_audience] || target_audience,
          enable_self_signed_jwt: options[:enable_self_signed_jwt] && options[:scope].nil?,
          token_credential_uri:   options[:token_credential_uri] || token_credential_uri,
          audience:               options[:audience] || audience
        }
        client = Google::Auth::DefaultCredentials.make_creds creds_input
        new client
      end

      private_class_method :from_env_vars,
                           :from_default_paths,
                           :from_application_default,
                           :from_io

      protected

      # Verify that the keyfile argument is provided.
      def verify_keyfile_provided! keyfile
        return unless keyfile.nil?
        raise "The keyfile passed to Google::Auth::Credentials.new was nil."
      end

      # Verify that the keyfile argument is a file.
      def verify_keyfile_exists! keyfile
        exists = ::File.file? keyfile
        raise "The keyfile '#{keyfile}' is not a valid file." unless exists
      end

      # Initializes the Signet client.
      def init_client keyfile, connection_options = {}
        client_opts = client_options keyfile
        Signet::OAuth2::Client.new(client_opts)
                              .configure_connection(connection_options)
      end

      # returns a new Hash with string keys instead of symbol keys.
      def stringify_hash_keys hash
        hash.to_h.transform_keys(&:to_s)
      end

      # rubocop:disable Metrics/AbcSize

      def client_options options
        # Keyfile options have higher priority over constructor defaults
        options["token_credential_uri"] ||= self.class.token_credential_uri
        options["audience"] ||= self.class.audience
        options["scope"] ||= self.class.scope
        options["target_audience"] ||= self.class.target_audience

        if !Array(options["scope"]).empty? && options["target_audience"]
          raise ArgumentError, "Cannot specify both scope and target_audience"
        end

        needs_scope = options["target_audience"].nil?
        # client options for initializing signet client
        { token_credential_uri: options["token_credential_uri"],
          audience:             options["audience"],
          scope:                (needs_scope ? Array(options["scope"]) : nil),
          target_audience:      options["target_audience"],
          issuer:               options["client_email"],
          signing_key:          OpenSSL::PKey::RSA.new(options["private_key"]) }
      end

      # rubocop:enable Metrics/AbcSize

      def update_from_signet client
        @project_id ||= client.project_id if client.respond_to? :project_id
        @quota_project_id ||= client.quota_project_id if client.respond_to? :quota_project_id
        @client = client
      end

      def update_from_hash hash, options
        hash = stringify_hash_keys hash
        hash["scope"] ||= options[:scope]
        hash["target_audience"] ||= options[:target_audience]
        @project_id ||= (hash["project_id"] || hash["project"])
        @quota_project_id ||= hash["quota_project_id"]
        @client = init_client hash, options
      end

      def update_from_filepath path, options
        verify_keyfile_exists! path
        json = JSON.parse ::File.read(path)
        json["scope"] ||= options[:scope]
        json["target_audience"] ||= options[:target_audience]
        @project_id ||= (json["project_id"] || json["project"])
        @quota_project_id ||= json["quota_project_id"]
        @client = init_client json, options
      end
    end
  end
end