googleauth 0.1.0 → 0.16.2

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 (78) 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/renovate.json +6 -0
  8. data/.github/sync-repo-settings.yaml +18 -0
  9. data/.github/workflows/ci.yml +55 -0
  10. data/.github/workflows/release-please.yml +39 -0
  11. data/.gitignore +3 -0
  12. data/.kokoro/populate-secrets.sh +76 -0
  13. data/.kokoro/release.cfg +52 -0
  14. data/.kokoro/release.sh +18 -0
  15. data/.kokoro/trampoline_v2.sh +489 -0
  16. data/.repo-metadata.json +5 -0
  17. data/.rubocop.yml +17 -0
  18. data/.toys/.toys.rb +45 -0
  19. data/.toys/ci.rb +43 -0
  20. data/.toys/kokoro/.toys.rb +66 -0
  21. data/.toys/kokoro/publish-docs.rb +67 -0
  22. data/.toys/kokoro/publish-gem.rb +53 -0
  23. data/.toys/linkinator.rb +43 -0
  24. data/.trampolinerc +48 -0
  25. data/CHANGELOG.md +199 -0
  26. data/CODE_OF_CONDUCT.md +43 -0
  27. data/Gemfile +22 -1
  28. data/{COPYING → LICENSE} +0 -0
  29. data/README.md +140 -17
  30. data/googleauth.gemspec +28 -28
  31. data/integration/helper.rb +31 -0
  32. data/integration/id_tokens/key_source_test.rb +74 -0
  33. data/lib/googleauth.rb +7 -37
  34. data/lib/googleauth/application_default.rb +81 -0
  35. data/lib/googleauth/client_id.rb +104 -0
  36. data/lib/googleauth/compute_engine.rb +73 -26
  37. data/lib/googleauth/credentials.rb +561 -0
  38. data/lib/googleauth/credentials_loader.rb +207 -0
  39. data/lib/googleauth/default_credentials.rb +93 -0
  40. data/lib/googleauth/iam.rb +75 -0
  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 +396 -0
  44. data/lib/googleauth/id_tokens/verifier.rb +142 -0
  45. data/lib/googleauth/json_key_reader.rb +50 -0
  46. data/lib/googleauth/scope_util.rb +61 -0
  47. data/lib/googleauth/service_account.rb +177 -67
  48. data/lib/googleauth/signet.rb +69 -8
  49. data/lib/googleauth/stores/file_token_store.rb +65 -0
  50. data/lib/googleauth/stores/redis_token_store.rb +96 -0
  51. data/lib/googleauth/token_store.rb +69 -0
  52. data/lib/googleauth/user_authorizer.rb +285 -0
  53. data/lib/googleauth/user_refresh.rb +129 -0
  54. data/lib/googleauth/version.rb +1 -1
  55. data/lib/googleauth/web_user_authorizer.rb +295 -0
  56. data/spec/googleauth/apply_auth_examples.rb +96 -94
  57. data/spec/googleauth/client_id_spec.rb +160 -0
  58. data/spec/googleauth/compute_engine_spec.rb +125 -55
  59. data/spec/googleauth/credentials_spec.rb +600 -0
  60. data/spec/googleauth/get_application_default_spec.rb +232 -80
  61. data/spec/googleauth/iam_spec.rb +80 -0
  62. data/spec/googleauth/scope_util_spec.rb +77 -0
  63. data/spec/googleauth/service_account_spec.rb +422 -68
  64. data/spec/googleauth/signet_spec.rb +101 -25
  65. data/spec/googleauth/stores/file_token_store_spec.rb +57 -0
  66. data/spec/googleauth/stores/redis_token_store_spec.rb +50 -0
  67. data/spec/googleauth/stores/store_examples.rb +58 -0
  68. data/spec/googleauth/user_authorizer_spec.rb +343 -0
  69. data/spec/googleauth/user_refresh_spec.rb +359 -0
  70. data/spec/googleauth/web_user_authorizer_spec.rb +172 -0
  71. data/spec/spec_helper.rb +51 -10
  72. data/test/helper.rb +33 -0
  73. data/test/id_tokens/key_sources_test.rb +240 -0
  74. data/test/id_tokens/verifier_test.rb +269 -0
  75. metadata +114 -75
  76. data/.travis.yml +0 -18
  77. data/CONTRIBUTING.md +0 -32
  78. 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