googleauth 0.1.0 → 0.16.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +5 -5
  2. data/.github/CODEOWNERS +7 -0
  3. data/.github/CONTRIBUTING.md +74 -0
  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/.github/workflows/ci.yml +55 -0
  8. data/.github/workflows/release-please.yml +39 -0
  9. data/.gitignore +3 -0
  10. data/.kokoro/populate-secrets.sh +76 -0
  11. data/.kokoro/release.cfg +52 -0
  12. data/.kokoro/release.sh +18 -0
  13. data/.kokoro/trampoline_v2.sh +489 -0
  14. data/.repo-metadata.json +5 -0
  15. data/.rubocop.yml +17 -0
  16. data/.toys/.toys.rb +45 -0
  17. data/.toys/ci.rb +43 -0
  18. data/.toys/kokoro/.toys.rb +66 -0
  19. data/.toys/kokoro/publish-docs.rb +67 -0
  20. data/.toys/kokoro/publish-gem.rb +53 -0
  21. data/.toys/linkinator.rb +43 -0
  22. data/.trampolinerc +48 -0
  23. data/CHANGELOG.md +192 -0
  24. data/CODE_OF_CONDUCT.md +43 -0
  25. data/Gemfile +22 -1
  26. data/{COPYING → LICENSE} +0 -0
  27. data/README.md +140 -17
  28. data/googleauth.gemspec +28 -28
  29. data/integration/helper.rb +31 -0
  30. data/integration/id_tokens/key_source_test.rb +74 -0
  31. data/lib/googleauth.rb +7 -37
  32. data/lib/googleauth/application_default.rb +81 -0
  33. data/lib/googleauth/client_id.rb +104 -0
  34. data/lib/googleauth/compute_engine.rb +73 -26
  35. data/lib/googleauth/credentials.rb +561 -0
  36. data/lib/googleauth/credentials_loader.rb +207 -0
  37. data/lib/googleauth/default_credentials.rb +93 -0
  38. data/lib/googleauth/iam.rb +75 -0
  39. data/lib/googleauth/id_tokens.rb +233 -0
  40. data/lib/googleauth/id_tokens/errors.rb +71 -0
  41. data/lib/googleauth/id_tokens/key_sources.rb +396 -0
  42. data/lib/googleauth/id_tokens/verifier.rb +142 -0
  43. data/lib/googleauth/json_key_reader.rb +50 -0
  44. data/lib/googleauth/scope_util.rb +61 -0
  45. data/lib/googleauth/service_account.rb +175 -67
  46. data/lib/googleauth/signet.rb +69 -8
  47. data/lib/googleauth/stores/file_token_store.rb +65 -0
  48. data/lib/googleauth/stores/redis_token_store.rb +96 -0
  49. data/lib/googleauth/token_store.rb +69 -0
  50. data/lib/googleauth/user_authorizer.rb +285 -0
  51. data/lib/googleauth/user_refresh.rb +129 -0
  52. data/lib/googleauth/version.rb +1 -1
  53. data/lib/googleauth/web_user_authorizer.rb +295 -0
  54. data/spec/googleauth/apply_auth_examples.rb +96 -94
  55. data/spec/googleauth/client_id_spec.rb +160 -0
  56. data/spec/googleauth/compute_engine_spec.rb +125 -55
  57. data/spec/googleauth/credentials_spec.rb +600 -0
  58. data/spec/googleauth/get_application_default_spec.rb +232 -80
  59. data/spec/googleauth/iam_spec.rb +80 -0
  60. data/spec/googleauth/scope_util_spec.rb +77 -0
  61. data/spec/googleauth/service_account_spec.rb +422 -68
  62. data/spec/googleauth/signet_spec.rb +101 -25
  63. data/spec/googleauth/stores/file_token_store_spec.rb +57 -0
  64. data/spec/googleauth/stores/redis_token_store_spec.rb +50 -0
  65. data/spec/googleauth/stores/store_examples.rb +58 -0
  66. data/spec/googleauth/user_authorizer_spec.rb +343 -0
  67. data/spec/googleauth/user_refresh_spec.rb +359 -0
  68. data/spec/googleauth/web_user_authorizer_spec.rb +172 -0
  69. data/spec/spec_helper.rb +51 -10
  70. data/test/helper.rb +33 -0
  71. data/test/id_tokens/key_sources_test.rb +240 -0
  72. data/test/id_tokens/verifier_test.rb +269 -0
  73. metadata +112 -75
  74. data/.travis.yml +0 -18
  75. data/CONTRIBUTING.md +0 -32
  76. data/Rakefile +0 -15
@@ -0,0 +1,81 @@
1
+ # Copyright 2015, 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 "googleauth/compute_engine"
31
+ require "googleauth/default_credentials"
32
+
33
+ module Google
34
+ # Module Auth provides classes that provide Google-specific authorization
35
+ # used to access Google APIs.
36
+ module Auth
37
+ NOT_FOUND_ERROR = <<~ERROR_MESSAGE.freeze
38
+ Could not load the default credentials. Browse to
39
+ https://developers.google.com/accounts/docs/application-default-credentials
40
+ for more information
41
+ ERROR_MESSAGE
42
+
43
+ module_function
44
+
45
+ # Obtains the default credentials implementation to use in this
46
+ # environment.
47
+ #
48
+ # Use this to obtain the Application Default Credentials for accessing
49
+ # Google APIs. Application Default Credentials are described in detail
50
+ # at https://cloud.google.com/docs/authentication/production.
51
+ #
52
+ # If supplied, scope is used to create the credentials instance, when it can
53
+ # be applied. E.g, on google compute engine and for user credentials the
54
+ # scope is ignored.
55
+ #
56
+ # @param scope [string|array|nil] the scope(s) to access
57
+ # @param options [Hash] Connection options. These may be used to configure
58
+ # the `Faraday::Connection` used for outgoing HTTP requests. For
59
+ # example, if a connection proxy must be used in the current network,
60
+ # you may provide a connection with with the needed proxy options.
61
+ # The following keys are recognized:
62
+ # * `:default_connection` The connection object to use for token
63
+ # refresh requests.
64
+ # * `:connection_builder` A `Proc` that creates and returns a
65
+ # connection to use for token refresh requests.
66
+ # * `:connection` The connection to use to determine whether GCE
67
+ # metadata credentials are available.
68
+ def get_application_default scope = nil, options = {}
69
+ creds = DefaultCredentials.from_env(scope, options) ||
70
+ DefaultCredentials.from_well_known_path(scope, options) ||
71
+ DefaultCredentials.from_system_default_path(scope, options)
72
+ return creds unless creds.nil?
73
+ unless GCECredentials.on_gce? options
74
+ # Clear cache of the result of GCECredentials.on_gce?
75
+ GCECredentials.unmemoize_all
76
+ raise NOT_FOUND_ERROR
77
+ end
78
+ GCECredentials.new scope: scope
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,104 @@
1
+ # Copyright 2014, 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 "multi_json"
31
+ require "googleauth/credentials_loader"
32
+
33
+ module Google
34
+ module Auth
35
+ # Representation of an application's identity for user authorization
36
+ # flows.
37
+ class ClientId
38
+ INSTALLED_APP = "installed".freeze
39
+ WEB_APP = "web".freeze
40
+ CLIENT_ID = "client_id".freeze
41
+ CLIENT_SECRET = "client_secret".freeze
42
+ MISSING_TOP_LEVEL_ELEMENT_ERROR =
43
+ "Expected top level property 'installed' or 'web' to be present.".freeze
44
+
45
+ # Text identifier of the client ID
46
+ # @return [String]
47
+ attr_reader :id
48
+
49
+ # Secret associated with the client ID
50
+ # @return [String]
51
+ attr_reader :secret
52
+
53
+ class << self
54
+ attr_accessor :default
55
+ end
56
+
57
+ # Initialize the Client ID
58
+ #
59
+ # @param [String] id
60
+ # Text identifier of the client ID
61
+ # @param [String] secret
62
+ # Secret associated with the client ID
63
+ # @note Direction instantion is discouraged to avoid embedding IDs
64
+ # & secrets in source. See {#from_file} to load from
65
+ # `client_secrets.json` files.
66
+ def initialize id, secret
67
+ CredentialsLoader.warn_if_cloud_sdk_credentials id
68
+ raise "Client id can not be nil" if id.nil?
69
+ raise "Client secret can not be nil" if secret.nil?
70
+ @id = id
71
+ @secret = secret
72
+ end
73
+
74
+ # Constructs a Client ID from a JSON file downloaded from the
75
+ # Google Developers Console.
76
+ #
77
+ # @param [String, File] file
78
+ # Path of file to read from
79
+ # @return [Google::Auth::ClientID]
80
+ def self.from_file file
81
+ raise "File can not be nil." if file.nil?
82
+ File.open file.to_s do |f|
83
+ json = f.read
84
+ config = MultiJson.load json
85
+ from_hash config
86
+ end
87
+ end
88
+
89
+ # Constructs a Client ID from a previously loaded JSON file. The hash
90
+ # structure should
91
+ # match the expected JSON format.
92
+ #
93
+ # @param [hash] config
94
+ # Parsed contents of the JSON file
95
+ # @return [Google::Auth::ClientID]
96
+ def self.from_hash config
97
+ raise "Hash can not be nil." if config.nil?
98
+ raw_detail = config[INSTALLED_APP] || config[WEB_APP]
99
+ raise MISSING_TOP_LEVEL_ELEMENT_ERROR if raw_detail.nil?
100
+ ClientId.new raw_detail[CLIENT_ID], raw_detail[CLIENT_SECRET]
101
+ end
102
+ end
103
+ end
104
+ end
@@ -27,46 +27,74 @@
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 'faraday'
31
- require 'googleauth/signet'
32
- require 'memoist'
30
+ require "faraday"
31
+ require "googleauth/signet"
32
+ require "memoist"
33
33
 
34
34
  module Google
35
35
  # Module Auth provides classes that provide Google-specific authorization
36
36
  # used to access Google APIs.
37
37
  module Auth
38
+ NO_METADATA_SERVER_ERROR = <<~ERROR.freeze
39
+ Error code 404 trying to get security access token
40
+ from Compute Engine metadata for the default service account. This
41
+ may be because the virtual machine instance does not have permission
42
+ scopes specified.
43
+ ERROR
44
+ UNEXPECTED_ERROR_SUFFIX = <<~ERROR.freeze
45
+ trying to get security access token from Compute Engine metadata for
46
+ the default service account
47
+ ERROR
48
+
38
49
  # Extends Signet::OAuth2::Client so that the auth token is obtained from
39
50
  # the GCE metadata server.
40
51
  class GCECredentials < Signet::OAuth2::Client
41
52
  # The IP Address is used in the URIs to speed up failures on non-GCE
42
53
  # systems.
43
- COMPUTE_AUTH_TOKEN_URI = 'http://169.254.169.254/computeMetadata/v1/'\
44
- 'instance/service-accounts/default/token'
45
- COMPUTE_CHECK_URI = 'http://169.254.169.254'
54
+ DEFAULT_METADATA_HOST = "169.254.169.254".freeze
55
+
56
+ # @private Unused and deprecated
57
+ COMPUTE_AUTH_TOKEN_URI =
58
+ "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token".freeze
59
+ # @private Unused and deprecated
60
+ COMPUTE_ID_TOKEN_URI =
61
+ "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/identity".freeze
62
+ # @private Unused and deprecated
63
+ COMPUTE_CHECK_URI = "http://169.254.169.254".freeze
46
64
 
47
65
  class << self
48
66
  extend Memoist
49
67
 
68
+ def metadata_host
69
+ ENV.fetch "GCE_METADATA_HOST", DEFAULT_METADATA_HOST
70
+ end
71
+
72
+ def compute_check_uri
73
+ "http://#{metadata_host}".freeze
74
+ end
75
+
76
+ def compute_auth_token_uri
77
+ "#{compute_check_uri}/computeMetadata/v1/instance/service-accounts/default/token".freeze
78
+ end
79
+
80
+ def compute_id_token_uri
81
+ "#{compute_check_uri}/computeMetadata/v1/instance/service-accounts/default/identity".freeze
82
+ end
83
+
50
84
  # Detect if this appear to be a GCE instance, by checking if metadata
51
- # is available
52
- def on_gce?(options = {})
85
+ # is available.
86
+ def on_gce? options = {}
87
+ # TODO: This should use google-cloud-env instead.
53
88
  c = options[:connection] || Faraday.default_connection
54
- resp = c.get(COMPUTE_CHECK_URI) do |req|
55
- # Comment from: oauth2client/client.py
56
- #
57
- # Note: the explicit `timeout` below is a workaround. The underlying
58
- # issue is that resolving an unknown host on some networks will take
59
- # 20-30 seconds; making this timeout short fixes the issue, but
60
- # could lead to false negatives in the event that we are on GCE, but
61
- # the metadata resolution was particularly slow. The latter case is
62
- # "unlikely".
63
- req.options.timeout = 0.1
89
+ headers = { "Metadata-Flavor" => "Google" }
90
+ resp = c.get compute_check_uri, nil, headers do |req|
91
+ req.options.timeout = 1.0
92
+ req.options.open_timeout = 0.1
64
93
  end
65
94
  return false unless resp.status == 200
66
- return false unless resp.headers.key?('Metadata-Flavor')
67
- return resp.headers['Metadata-Flavor'] == 'Google'
95
+ resp.headers["Metadata-Flavor"] == "Google"
68
96
  rescue Faraday::TimeoutError, Faraday::ConnectionFailed
69
- return false
97
+ false
70
98
  end
71
99
 
72
100
  memoize :on_gce?
@@ -74,12 +102,31 @@ module Google
74
102
 
75
103
  # Overrides the super class method to change how access tokens are
76
104
  # fetched.
77
- def fetch_access_token(options = {})
105
+ def fetch_access_token options = {}
78
106
  c = options[:connection] || Faraday.default_connection
79
- c.headers = { 'Metadata-Flavor' => 'Google' }
80
- resp = c.get(COMPUTE_AUTH_TOKEN_URI)
81
- Signet::OAuth2.parse_credentials(resp.body,
82
- resp.headers['content-type'])
107
+ retry_with_error do
108
+ uri = target_audience ? GCECredentials.compute_id_token_uri : GCECredentials.compute_auth_token_uri
109
+ query = target_audience ? { "audience" => target_audience, "format" => "full" } : {}
110
+ query[:scopes] = Array(scope).join "," if scope
111
+ resp = c.get uri, query, "Metadata-Flavor" => "Google"
112
+ case resp.status
113
+ when 200
114
+ content_type = resp.headers["content-type"]
115
+ if ["text/html", "application/text"].include? content_type
116
+ { (target_audience ? "id_token" : "access_token") => resp.body }
117
+ else
118
+ Signet::OAuth2.parse_credentials resp.body, content_type
119
+ end
120
+ when 403, 500
121
+ msg = "Unexpected error code #{resp.status} #{UNEXPECTED_ERROR_SUFFIX}"
122
+ raise Signet::UnexpectedStatusError, msg
123
+ when 404
124
+ raise Signet::AuthorizationError, NO_METADATA_SERVER_ERROR
125
+ else
126
+ msg = "Unexpected error code #{resp.status} #{UNEXPECTED_ERROR_SUFFIX}"
127
+ raise Signet::AuthorizationError, msg
128
+ end
129
+ end
83
130
  end
84
131
  end
85
132
  end
@@ -0,0 +1,561 @@
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 a high-level base class used by Google's API client
40
+ # libraries to represent the authentication when connecting to an API.
41
+ # In most cases, it is subclassed by API-specific credential classes that
42
+ # can be instantiated by clients.
43
+ #
44
+ # ## Options
45
+ #
46
+ # Credentials classes are configured with options that dictate default
47
+ # values for parameters such as scope and audience. These defaults are
48
+ # expressed as class attributes, and may differ from endpoint to endpoint.
49
+ # Normally, an API client will provide subclasses specific to each
50
+ # endpoint, configured with appropriate values.
51
+ #
52
+ # Note that these options inherit up the class hierarchy. If a particular
53
+ # options is not set for a subclass, its superclass is queried.
54
+ #
55
+ # Some older users of this class set options via constants. This usage is
56
+ # deprecated. For example, instead of setting the `AUDIENCE` constant on
57
+ # your subclass, call the `audience=` method.
58
+ #
59
+ # ## Example
60
+ #
61
+ # class MyCredentials < Google::Auth::Credentials
62
+ # # Set the default scope for these credentials
63
+ # self.scope = "http://example.com/my_scope"
64
+ # end
65
+ #
66
+ # # creds is a credentials object suitable for Google API clients
67
+ # creds = MyCredentials.default
68
+ # creds.scope # => ["http://example.com/my_scope"]
69
+ #
70
+ # class SubCredentials < MyCredentials
71
+ # # Override the default scope for this subclass
72
+ # self.scope = "http://example.com/sub_scope"
73
+ # end
74
+ #
75
+ # creds2 = SubCredentials.default
76
+ # creds2.scope # => ["http://example.com/sub_scope"]
77
+ #
78
+ class Credentials # rubocop:disable Metrics/ClassLength
79
+ ##
80
+ # The default token credential URI to be used when none is provided during initialization.
81
+ TOKEN_CREDENTIAL_URI = "https://oauth2.googleapis.com/token".freeze
82
+
83
+ ##
84
+ # The default target audience ID to be used when none is provided during initialization.
85
+ AUDIENCE = "https://oauth2.googleapis.com/token".freeze
86
+
87
+ @audience = @scope = @target_audience = @env_vars = @paths = @token_credential_uri = nil
88
+
89
+ ##
90
+ # The default token credential URI to be used when none is provided during initialization.
91
+ # The URI is the authorization server's HTTP endpoint capable of issuing tokens and
92
+ # refreshing expired tokens.
93
+ #
94
+ # @return [String]
95
+ #
96
+ def self.token_credential_uri
97
+ lookup_auth_param :token_credential_uri do
98
+ lookup_local_constant :TOKEN_CREDENTIAL_URI
99
+ end
100
+ end
101
+
102
+ ##
103
+ # Set the default token credential URI to be used when none is provided during initialization.
104
+ #
105
+ # @param [String] new_token_credential_uri
106
+ #
107
+ def self.token_credential_uri= new_token_credential_uri
108
+ @token_credential_uri = new_token_credential_uri
109
+ end
110
+
111
+ ##
112
+ # The default target audience ID to be used when none is provided during initialization.
113
+ # Used only by the assertion grant type.
114
+ #
115
+ # @return [String]
116
+ #
117
+ def self.audience
118
+ lookup_auth_param :audience do
119
+ lookup_local_constant :AUDIENCE
120
+ end
121
+ end
122
+
123
+ ##
124
+ # Sets the default target audience ID to be used when none is provided during initialization.
125
+ #
126
+ # @param [String] new_audience
127
+ #
128
+ def self.audience= new_audience
129
+ @audience = new_audience
130
+ end
131
+
132
+ ##
133
+ # The default scope to be used when none is provided during initialization.
134
+ # A scope is an access range defined by the authorization server.
135
+ # The scope can be a single value or a list of values.
136
+ #
137
+ # Either {#scope} or {#target_audience}, but not both, should be non-nil.
138
+ # If {#scope} is set, this credential will produce access tokens.
139
+ # If {#target_audience} is set, this credential will produce ID tokens.
140
+ #
141
+ # @return [String, Array<String>, nil]
142
+ #
143
+ def self.scope
144
+ lookup_auth_param :scope do
145
+ vals = lookup_local_constant :SCOPE
146
+ vals ? Array(vals).flatten.uniq : nil
147
+ end
148
+ end
149
+
150
+ ##
151
+ # Sets the default scope to be used when none is provided during initialization.
152
+ #
153
+ # Either {#scope} or {#target_audience}, but not both, should be non-nil.
154
+ # If {#scope} is set, this credential will produce access tokens.
155
+ # If {#target_audience} is set, this credential will produce ID tokens.
156
+ #
157
+ # @param [String, Array<String>, nil] new_scope
158
+ #
159
+ def self.scope= new_scope
160
+ new_scope = Array new_scope unless new_scope.nil?
161
+ @scope = new_scope
162
+ end
163
+
164
+ ##
165
+ # The default final target audience for ID tokens, to be used when none
166
+ # is provided during initialization.
167
+ #
168
+ # Either {#scope} or {#target_audience}, but not both, should be non-nil.
169
+ # If {#scope} is set, this credential will produce access tokens.
170
+ # If {#target_audience} is set, this credential will produce ID tokens.
171
+ #
172
+ # @return [String, nil]
173
+ #
174
+ def self.target_audience
175
+ lookup_auth_param :target_audience
176
+ end
177
+
178
+ ##
179
+ # Sets the default final target audience for ID tokens, to be used when none
180
+ # is provided during initialization.
181
+ #
182
+ # Either {#scope} or {#target_audience}, but not both, should be non-nil.
183
+ # If {#scope} is set, this credential will produce access tokens.
184
+ # If {#target_audience} is set, this credential will produce ID tokens.
185
+ #
186
+ # @param [String, nil] new_target_audience
187
+ #
188
+ def self.target_audience= new_target_audience
189
+ @target_audience = new_target_audience
190
+ end
191
+
192
+ ##
193
+ # The environment variables to search for credentials. Values can either be a file path to the
194
+ # credentials file, or the JSON contents of the credentials file.
195
+ # The env_vars will never be nil. If there are no vars, the empty array is returned.
196
+ #
197
+ # @return [Array<String>]
198
+ #
199
+ def self.env_vars
200
+ env_vars_internal || []
201
+ end
202
+
203
+ ##
204
+ # @private
205
+ # Internal recursive lookup for env_vars.
206
+ #
207
+ def self.env_vars_internal
208
+ lookup_auth_param :env_vars, :env_vars_internal do
209
+ # Pull values when PATH_ENV_VARS or JSON_ENV_VARS constants exists.
210
+ path_env_vars = lookup_local_constant :PATH_ENV_VARS
211
+ json_env_vars = lookup_local_constant :JSON_ENV_VARS
212
+ (Array(path_env_vars) + Array(json_env_vars)).flatten.uniq if path_env_vars || json_env_vars
213
+ end
214
+ end
215
+
216
+ ##
217
+ # Sets the environment variables to search for credentials.
218
+ # Setting to `nil` "unsets" the value, and defaults to the superclass
219
+ # (or to the empty array if there is no superclass).
220
+ #
221
+ # @param [String, Array<String>, nil] new_env_vars
222
+ #
223
+ def self.env_vars= new_env_vars
224
+ new_env_vars = Array new_env_vars unless new_env_vars.nil?
225
+ @env_vars = new_env_vars
226
+ end
227
+
228
+ ##
229
+ # The file paths to search for credentials files.
230
+ # The paths will never be nil. If there are no paths, the empty array is returned.
231
+ #
232
+ # @return [Array<String>]
233
+ #
234
+ def self.paths
235
+ paths_internal || []
236
+ end
237
+
238
+ ##
239
+ # @private
240
+ # Internal recursive lookup for paths.
241
+ #
242
+ def self.paths_internal
243
+ lookup_auth_param :paths, :paths_internal do
244
+ # Pull in values if the DEFAULT_PATHS constant exists.
245
+ vals = lookup_local_constant :DEFAULT_PATHS
246
+ vals ? Array(vals).flatten.uniq : nil
247
+ end
248
+ end
249
+
250
+ ##
251
+ # Set the file paths to search for credentials files.
252
+ # Setting to `nil` "unsets" the value, and defaults to the superclass
253
+ # (or to the empty array if there is no superclass).
254
+ #
255
+ # @param [String, Array<String>, nil] new_paths
256
+ #
257
+ def self.paths= new_paths
258
+ new_paths = Array new_paths unless new_paths.nil?
259
+ @paths = new_paths
260
+ end
261
+
262
+ ##
263
+ # @private
264
+ # Return the given parameter value, defaulting up the class hierarchy.
265
+ #
266
+ # First returns the value of the instance variable, if set.
267
+ # Next, calls the given block if provided. (This is generally used to
268
+ # look up legacy constant-based values.)
269
+ # Otherwise, calls the superclass method if present.
270
+ # Returns nil if all steps fail.
271
+ #
272
+ # @param name [Symbol] The parameter name
273
+ # @param method_name [Symbol] The lookup method name, if different
274
+ # @return [Object] The value
275
+ #
276
+ def self.lookup_auth_param name, method_name = name
277
+ val = instance_variable_get "@#{name}".to_sym
278
+ val = yield if val.nil? && block_given?
279
+ return val unless val.nil?
280
+ return superclass.send method_name if superclass.respond_to? method_name
281
+ nil
282
+ end
283
+
284
+ ##
285
+ # @private
286
+ # Return the value of the given constant if it is defined directly in
287
+ # this class, or nil if not.
288
+ #
289
+ # @param [Symbol] Name of the constant
290
+ # @return [Object] The value
291
+ #
292
+ def self.lookup_local_constant name
293
+ const_defined?(name, false) ? const_get(name) : nil
294
+ end
295
+
296
+ ##
297
+ # The Signet::OAuth2::Client object the Credentials instance is using.
298
+ #
299
+ # @return [Signet::OAuth2::Client]
300
+ #
301
+ attr_accessor :client
302
+
303
+ ##
304
+ # Identifier for the project the client is authenticating with.
305
+ #
306
+ # @return [String]
307
+ #
308
+ attr_reader :project_id
309
+
310
+ ##
311
+ # Identifier for a separate project used for billing/quota, if any.
312
+ #
313
+ # @return [String,nil]
314
+ #
315
+ attr_reader :quota_project_id
316
+
317
+ # @private Delegate client methods to the client object.
318
+ extend Forwardable
319
+
320
+ ##
321
+ # @!attribute [r] token_credential_uri
322
+ # @return [String] The token credential URI. The URI is the authorization server's HTTP
323
+ # endpoint capable of issuing tokens and refreshing expired tokens.
324
+ #
325
+ # @!attribute [r] audience
326
+ # @return [String] The target audience ID when issuing assertions. Used only by the
327
+ # assertion grant type.
328
+ #
329
+ # @!attribute [r] scope
330
+ # @return [String, Array<String>] The scope for this client. A scope is an access range
331
+ # defined by the authorization server. The scope can be a single value or a list of values.
332
+ #
333
+ # @!attribute [r] target_audience
334
+ # @return [String] The final target audience for ID tokens returned by this credential.
335
+ #
336
+ # @!attribute [r] issuer
337
+ # @return [String] The issuer ID associated with this client.
338
+ #
339
+ # @!attribute [r] signing_key
340
+ # @return [String, OpenSSL::PKey] The signing key associated with this client.
341
+ #
342
+ # @!attribute [r] updater_proc
343
+ # @return [Proc] Returns a reference to the {Signet::OAuth2::Client#apply} method,
344
+ # suitable for passing as a closure.
345
+ #
346
+ def_delegators :@client,
347
+ :token_credential_uri, :audience,
348
+ :scope, :issuer, :signing_key, :updater_proc, :target_audience
349
+
350
+ ##
351
+ # Creates a new Credentials instance with the provided auth credentials, and with the default
352
+ # values configured on the class.
353
+ #
354
+ # @param [String, Hash, Signet::OAuth2::Client] keyfile
355
+ # The keyfile can be provided as one of the following:
356
+ #
357
+ # * The path to a JSON keyfile (as a +String+)
358
+ # * The contents of a JSON keyfile (as a +Hash+)
359
+ # * A +Signet::OAuth2::Client+ object
360
+ # @param [Hash] options
361
+ # The options for configuring the credentials instance. The following is supported:
362
+ #
363
+ # * +:scope+ - the scope for the client
364
+ # * +"project_id"+ (and optionally +"project"+) - the project identifier for the client
365
+ # * +:connection_builder+ - the connection builder to use for the client
366
+ # * +:default_connection+ - the default connection to use for the client
367
+ #
368
+ def initialize keyfile, options = {}
369
+ verify_keyfile_provided! keyfile
370
+ @project_id = options["project_id"] || options["project"]
371
+ @quota_project_id = options["quota_project_id"]
372
+ case keyfile
373
+ when Signet::OAuth2::Client
374
+ update_from_signet keyfile
375
+ when Hash
376
+ update_from_hash keyfile, options
377
+ else
378
+ update_from_filepath keyfile, options
379
+ end
380
+ CredentialsLoader.warn_if_cloud_sdk_credentials @client.client_id
381
+ @project_id ||= CredentialsLoader.load_gcloud_project_id
382
+ @client.fetch_access_token!
383
+ @env_vars = nil
384
+ @paths = nil
385
+ @scope = nil
386
+ end
387
+
388
+ ##
389
+ # Creates a new Credentials instance with auth credentials acquired by searching the
390
+ # environment variables and paths configured on the class, and with the default values
391
+ # configured on the class.
392
+ #
393
+ # The auth credentials are searched for in the following order:
394
+ #
395
+ # 1. configured environment variables (see {Credentials.env_vars})
396
+ # 2. configured default file paths (see {Credentials.paths})
397
+ # 3. application default (see {Google::Auth.get_application_default})
398
+ #
399
+ # @param [Hash] options
400
+ # The options for configuring the credentials instance. The following is supported:
401
+ #
402
+ # * +:scope+ - the scope for the client
403
+ # * +"project_id"+ (and optionally +"project"+) - the project identifier for the client
404
+ # * +:connection_builder+ - the connection builder to use for the client
405
+ # * +:default_connection+ - the default connection to use for the client
406
+ #
407
+ # @return [Credentials]
408
+ #
409
+ def self.default options = {}
410
+ # First try to find keyfile file or json from environment variables.
411
+ client = from_env_vars options
412
+
413
+ # Second try to find keyfile file from known file paths.
414
+ client ||= from_default_paths options
415
+
416
+ # Finally get instantiated client from Google::Auth
417
+ client ||= from_application_default options
418
+ client
419
+ end
420
+
421
+ ##
422
+ # @private Lookup Credentials from environment variables.
423
+ def self.from_env_vars options
424
+ env_vars.each do |env_var|
425
+ str = ENV[env_var]
426
+ next if str.nil?
427
+ io =
428
+ if ::File.file? str
429
+ ::StringIO.new ::File.read str
430
+ else
431
+ json = ::JSON.parse str rescue nil
432
+ json ? ::StringIO.new(str) : nil
433
+ end
434
+ next if io.nil?
435
+ return from_io io, options
436
+ end
437
+ nil
438
+ end
439
+
440
+ ##
441
+ # @private Lookup Credentials from default file paths.
442
+ def self.from_default_paths options
443
+ paths.each do |path|
444
+ next unless path && ::File.file?(path)
445
+ io = ::StringIO.new ::File.read path
446
+ return from_io io, options
447
+ end
448
+ nil
449
+ end
450
+
451
+ ##
452
+ # @private Lookup Credentials using Google::Auth.get_application_default.
453
+ def self.from_application_default options
454
+ scope = options[:scope] || self.scope
455
+ auth_opts = {
456
+ token_credential_uri: options[:token_credential_uri] || token_credential_uri,
457
+ audience: options[:audience] || audience,
458
+ target_audience: options[:target_audience] || target_audience,
459
+ enable_self_signed_jwt: options[:enable_self_signed_jwt] && options[:scope].nil?
460
+ }
461
+ client = Google::Auth.get_application_default scope, auth_opts
462
+ new client, options
463
+ end
464
+
465
+ # @private Read credentials from a JSON stream.
466
+ def self.from_io io, options
467
+ creds_input = {
468
+ json_key_io: io,
469
+ scope: options[:scope] || scope,
470
+ target_audience: options[:target_audience] || target_audience,
471
+ enable_self_signed_jwt: options[:enable_self_signed_jwt] && options[:scope].nil?,
472
+ token_credential_uri: options[:token_credential_uri] || token_credential_uri,
473
+ audience: options[:audience] || audience
474
+ }
475
+ client = Google::Auth::DefaultCredentials.make_creds creds_input
476
+ new client
477
+ end
478
+
479
+ private_class_method :from_env_vars,
480
+ :from_default_paths,
481
+ :from_application_default,
482
+ :from_io
483
+
484
+ protected
485
+
486
+ # Verify that the keyfile argument is provided.
487
+ def verify_keyfile_provided! keyfile
488
+ return unless keyfile.nil?
489
+ raise "The keyfile passed to Google::Auth::Credentials.new was nil."
490
+ end
491
+
492
+ # Verify that the keyfile argument is a file.
493
+ def verify_keyfile_exists! keyfile
494
+ exists = ::File.file? keyfile
495
+ raise "The keyfile '#{keyfile}' is not a valid file." unless exists
496
+ end
497
+
498
+ # Initializes the Signet client.
499
+ def init_client keyfile, connection_options = {}
500
+ client_opts = client_options keyfile
501
+ Signet::OAuth2::Client.new(client_opts)
502
+ .configure_connection(connection_options)
503
+ end
504
+
505
+ # returns a new Hash with string keys instead of symbol keys.
506
+ def stringify_hash_keys hash
507
+ hash.to_h.transform_keys(&:to_s)
508
+ end
509
+
510
+ # rubocop:disable Metrics/AbcSize
511
+
512
+ def client_options options
513
+ # Keyfile options have higher priority over constructor defaults
514
+ options["token_credential_uri"] ||= self.class.token_credential_uri
515
+ options["audience"] ||= self.class.audience
516
+ options["scope"] ||= self.class.scope
517
+ options["target_audience"] ||= self.class.target_audience
518
+
519
+ if !Array(options["scope"]).empty? && options["target_audience"]
520
+ raise ArgumentError, "Cannot specify both scope and target_audience"
521
+ end
522
+
523
+ needs_scope = options["target_audience"].nil?
524
+ # client options for initializing signet client
525
+ { token_credential_uri: options["token_credential_uri"],
526
+ audience: options["audience"],
527
+ scope: (needs_scope ? Array(options["scope"]) : nil),
528
+ target_audience: options["target_audience"],
529
+ issuer: options["client_email"],
530
+ signing_key: OpenSSL::PKey::RSA.new(options["private_key"]) }
531
+ end
532
+
533
+ # rubocop:enable Metrics/AbcSize
534
+
535
+ def update_from_signet client
536
+ @project_id ||= client.project_id if client.respond_to? :project_id
537
+ @quota_project_id ||= client.quota_project_id if client.respond_to? :quota_project_id
538
+ @client = client
539
+ end
540
+
541
+ def update_from_hash hash, options
542
+ hash = stringify_hash_keys hash
543
+ hash["scope"] ||= options[:scope]
544
+ hash["target_audience"] ||= options[:target_audience]
545
+ @project_id ||= (hash["project_id"] || hash["project"])
546
+ @quota_project_id ||= hash["quota_project_id"]
547
+ @client = init_client hash, options
548
+ end
549
+
550
+ def update_from_filepath path, options
551
+ verify_keyfile_exists! path
552
+ json = JSON.parse ::File.read(path)
553
+ json["scope"] ||= options[:scope]
554
+ json["target_audience"] ||= options[:target_audience]
555
+ @project_id ||= (json["project_id"] || json["project"])
556
+ @quota_project_id ||= json["quota_project_id"]
557
+ @client = init_client json, options
558
+ end
559
+ end
560
+ end
561
+ end