Resolve merge conflict, update rspec syntax and docs
[arvados.git] / lib / google / api_client.rb
1 # Copyright 2010 Google Inc.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 #      http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15
16 require 'faraday'
17 require 'multi_json'
18 require 'compat/multi_json'
19 require 'stringio'
20 require 'retriable'
21
22 require 'google/api_client/version'
23 require 'google/api_client/logging'
24 require 'google/api_client/errors'
25 require 'google/api_client/environment'
26 require 'google/api_client/discovery'
27 require 'google/api_client/request'
28 require 'google/api_client/reference'
29 require 'google/api_client/result'
30 require 'google/api_client/media'
31 require 'google/api_client/service_account'
32 require 'google/api_client/batch'
33 require 'google/api_client/gzip'
34 require 'google/api_client/client_secrets'
35 require 'google/api_client/railtie' if defined?(Rails)
36
37 module Google
38
39   ##
40   # This class manages APIs communication.
41   class APIClient
42     include Google::APIClient::Logging
43     
44     ##
45     # Creates a new Google API client.
46     #
47     # @param [Hash] options The configuration parameters for the client.
48     # @option options [Symbol, #generate_authenticated_request] :authorization
49     #   (:oauth_1)
50     #   The authorization mechanism used by the client.  The following
51     #   mechanisms are supported out-of-the-box:
52     #   <ul>
53     #     <li><code>:two_legged_oauth_1</code></li>
54     #     <li><code>:oauth_1</code></li>
55     #     <li><code>:oauth_2</code></li>
56     #   </ul>
57     # @option options [Boolean] :auto_refresh_token (true)
58     #   The setting that controls whether or not the api client attempts to
59     #   refresh authorization when a 401 is hit in #execute. If the token does 
60     #   not support it, this option is ignored.
61     # @option options [String] :application_name
62     #   The name of the application using the client.
63     # @option options [String] :application_version
64     #   The version number of the application using the client.
65     # @option options [String] :user_agent
66     #   ("{app_name} google-api-ruby-client/{version} {os_name}/{os_version}")
67     #   The user agent used by the client.  Most developers will want to
68     #   leave this value alone and use the `:application_name` option instead.
69     # @option options [String] :host ("www.googleapis.com")
70     #   The API hostname used by the client. This rarely needs to be changed.
71     # @option options [String] :port (443)
72     #   The port number used by the client. This rarely needs to be changed.
73     # @option options [String] :discovery_path ("/discovery/v1")
74     #   The discovery base path. This rarely needs to be changed.
75     # @option options [String] :ca_file
76     #   Optional set of root certificates to use when validating SSL connections.
77     #   By default, a bundled set of trusted roots will be used.
78     # @options options[Hash] :faraday_options
79     #   Pass through of options to set on the Faraday connection
80     def initialize(options={})
81       logger.debug { "#{self.class} - Initializing client with options #{options}" }
82       
83       # Normalize key to String to allow indifferent access.
84       options = options.inject({}) do |accu, (key, value)|
85         accu[key.to_sym] = value
86         accu
87       end
88       # Almost all API usage will have a host of 'www.googleapis.com'.
89       self.host = options[:host] || 'www.googleapis.com'
90       self.port = options[:port] || 443
91       self.discovery_path = options[:discovery_path] || '/discovery/v1'
92
93       # Most developers will want to leave this value alone and use the
94       # application_name option.
95       if options[:application_name]
96         app_name = options[:application_name]
97         app_version = options[:application_version]
98         application_string = "#{app_name}/#{app_version || '0.0.0'}"
99       else
100         logger.warn { "#{self.class} - Please provide :application_name and :application_version when initializing the client" }
101       end
102
103       proxy = options[:proxy] || Object::ENV["http_proxy"]
104
105       self.user_agent = options[:user_agent] || (
106         "#{application_string} " +
107         "google-api-ruby-client/#{Google::APIClient::VERSION::STRING} #{ENV::OS_VERSION} (gzip)"
108       ).strip
109       # The writer method understands a few Symbols and will generate useful
110       # default authentication mechanisms.
111       self.authorization =
112         options.key?(:authorization) ? options[:authorization] : :oauth_2
113       self.auto_refresh_token = options.fetch(:auto_refresh_token) { true }
114       self.key = options[:key]
115       self.user_ip = options[:user_ip]
116       self.retries = options.fetch(:retries) { 0 }
117       @discovery_uris = {}
118       @discovery_documents = {}
119       @discovered_apis = {}
120       ca_file = options[:ca_file] || File.expand_path('../../cacerts.pem', __FILE__)
121       self.connection = Faraday.new do |faraday|
122         faraday.response :gzip
123         faraday.options.params_encoder = Faraday::FlatParamsEncoder
124         faraday.ssl.ca_file = ca_file
125         faraday.ssl.verify = true
126         faraday.proxy proxy
127         faraday.adapter Faraday.default_adapter
128         if options[:faraday_option].is_a?(Hash)
129           options[:faraday_option].each_pair do |option, value|
130             faraday.options.send("#{option}=", value)
131           end
132         end
133       end
134       return self
135     end
136
137     ##
138     # Returns the authorization mechanism used by the client.
139     #
140     # @return [#generate_authenticated_request] The authorization mechanism.
141     attr_reader :authorization
142
143     ##
144     # Sets the authorization mechanism used by the client.
145     #
146     # @param [#generate_authenticated_request] new_authorization
147     #   The new authorization mechanism.
148     def authorization=(new_authorization)
149       case new_authorization
150       when :oauth_1, :oauth
151         require 'signet/oauth_1/client'
152         # NOTE: Do not rely on this default value, as it may change
153         new_authorization = Signet::OAuth1::Client.new(
154           :temporary_credential_uri =>
155             'https://www.google.com/accounts/OAuthGetRequestToken',
156           :authorization_uri =>
157             'https://www.google.com/accounts/OAuthAuthorizeToken',
158           :token_credential_uri =>
159             'https://www.google.com/accounts/OAuthGetAccessToken',
160           :client_credential_key => 'anonymous',
161           :client_credential_secret => 'anonymous'
162         )
163       when :two_legged_oauth_1, :two_legged_oauth
164         require 'signet/oauth_1/client'
165         # NOTE: Do not rely on this default value, as it may change
166         new_authorization = Signet::OAuth1::Client.new(
167           :client_credential_key => nil,
168           :client_credential_secret => nil,
169           :two_legged => true
170         )
171       when :oauth_2
172         require 'signet/oauth_2/client'
173         # NOTE: Do not rely on this default value, as it may change
174         new_authorization = Signet::OAuth2::Client.new(
175           :authorization_uri =>
176             'https://accounts.google.com/o/oauth2/auth',
177           :token_credential_uri =>
178             'https://accounts.google.com/o/oauth2/token'
179         )
180       when nil
181         # No authorization mechanism
182       else
183         if !new_authorization.respond_to?(:generate_authenticated_request)
184           raise TypeError,
185             'Expected authorization mechanism to respond to ' +
186             '#generate_authenticated_request.'
187         end
188       end
189       @authorization = new_authorization
190       return @authorization
191     end
192
193     ##
194     # Default Faraday/HTTP connection.
195     #
196     # @return [Faraday::Connection]
197     attr_accessor :connection
198
199     ##
200     # The setting that controls whether or not the api client attempts to
201     # refresh authorization when a 401 is hit in #execute. 
202     #
203     # @return [Boolean]
204     attr_accessor :auto_refresh_token
205
206     ##
207     # The application's API key issued by the API console.
208     #
209     # @return [String] The API key.
210     attr_accessor :key
211
212     ##
213     # The IP address of the user this request is being performed on behalf of.
214     #
215     # @return [String] The user's IP address.
216     attr_accessor :user_ip
217
218     ##
219     # The user agent used by the client.
220     #
221     # @return [String]
222     #   The user agent string used in the User-Agent header.
223     attr_accessor :user_agent
224
225     ##
226     # The API hostname used by the client.
227     #
228     # @return [String]
229     #   The API hostname. Should almost always be 'www.googleapis.com'.
230     attr_accessor :host
231
232     ##
233     # The port number used by the client.
234     #
235     # @return [String]
236     #   The port number. Should almost always be 443.
237     attr_accessor :port
238
239     ##
240     # The base path used by the client for discovery.
241     #
242     # @return [String]
243     #   The base path. Should almost always be '/discovery/v1'.
244     attr_accessor :discovery_path
245
246     ##
247     # Number of times to retry on recoverable errors
248     # 
249     # @return [FixNum]
250     #  Number of retries
251     attr_accessor :retries
252
253     ##
254     # Returns the URI for the directory document.
255     #
256     # @return [Addressable::URI] The URI of the directory document.
257     def directory_uri
258       return resolve_uri(self.discovery_path + '/apis')
259     end
260
261     ##
262     # Manually registers a URI as a discovery document for a specific version
263     # of an API.
264     #
265     # @param [String, Symbol] api The API name.
266     # @param [String] version The desired version of the API.
267     # @param [Addressable::URI] uri The URI of the discovery document.
268     def register_discovery_uri(api, version, uri)
269       api = api.to_s
270       version = version || 'v1'
271       @discovery_uris["#{api}:#{version}"] = uri
272     end
273
274     ##
275     # Returns the URI for the discovery document.
276     #
277     # @param [String, Symbol] api The API name.
278     # @param [String] version The desired version of the API.
279     # @return [Addressable::URI] The URI of the discovery document.
280     def discovery_uri(api, version=nil)
281       api = api.to_s
282       version = version || 'v1'
283       return @discovery_uris["#{api}:#{version}"] ||= (
284         resolve_uri(
285           self.discovery_path + '/apis/{api}/{version}/rest',
286           'api' => api,
287           'version' => version
288         )
289       )
290     end
291
292     ##
293     # Manually registers a pre-loaded discovery document for a specific version
294     # of an API.
295     #
296     # @param [String, Symbol] api The API name.
297     # @param [String] version The desired version of the API.
298     # @param [String, StringIO] discovery_document
299     #   The contents of the discovery document.
300     def register_discovery_document(api, version, discovery_document)
301       api = api.to_s
302       version = version || 'v1'
303       if discovery_document.kind_of?(StringIO)
304         discovery_document.rewind
305         discovery_document = discovery_document.string
306       elsif discovery_document.respond_to?(:to_str)
307         discovery_document = discovery_document.to_str
308       else
309         raise TypeError,
310           "Expected String or StringIO, got #{discovery_document.class}."
311       end
312       @discovery_documents["#{api}:#{version}"] =
313         MultiJson.load(discovery_document)
314     end
315
316     ##
317     # Returns the parsed directory document.
318     #
319     # @return [Hash] The parsed JSON from the directory document.
320     def directory_document
321       return @directory_document ||= (begin
322         response = self.execute!(
323           :http_method => :get,
324           :uri => self.directory_uri,
325           :authenticated => false
326         )
327         response.data
328       end)
329     end
330
331     ##
332     # Returns the parsed discovery document.
333     #
334     # @param [String, Symbol] api The API name.
335     # @param [String] version The desired version of the API.
336     # @return [Hash] The parsed JSON from the discovery document.
337     def discovery_document(api, version=nil)
338       api = api.to_s
339       version = version || 'v1'
340       return @discovery_documents["#{api}:#{version}"] ||= (begin
341         response = self.execute!(
342           :http_method => :get,
343           :uri => self.discovery_uri(api, version),
344           :authenticated => false
345         )
346         response.data
347       end)
348     end
349
350     ##
351     # Returns all APIs published in the directory document.
352     #
353     # @return [Array] The list of available APIs.
354     def discovered_apis
355       @directory_apis ||= (begin
356         document_base = self.directory_uri
357         if self.directory_document && self.directory_document['items']
358           self.directory_document['items'].map do |discovery_document|
359             Google::APIClient::API.new(
360               document_base,
361               discovery_document
362             )
363           end
364         else
365           []
366         end
367       end)
368     end
369
370     ##
371     # Returns the service object for a given service name and service version.
372     #
373     # @param [String, Symbol] api The API name.
374     # @param [String] version The desired version of the API.
375     #
376     # @return [Google::APIClient::API] The service object.
377     def discovered_api(api, version=nil)
378       if !api.kind_of?(String) && !api.kind_of?(Symbol)
379         raise TypeError,
380           "Expected String or Symbol, got #{api.class}."
381       end
382       api = api.to_s
383       version = version || 'v1'
384       return @discovered_apis["#{api}:#{version}"] ||= begin
385         document_base = self.discovery_uri(api, version)
386         discovery_document = self.discovery_document(api, version)
387         if document_base && discovery_document
388           Google::APIClient::API.new(
389             document_base,
390             discovery_document
391           )
392         else
393           nil
394         end
395       end
396     end
397
398     ##
399     # Returns the method object for a given RPC name and service version.
400     #
401     # @param [String, Symbol] rpc_name The RPC name of the desired method.
402     # @param [String, Symbol] api The API the method is within.
403     # @param [String] version The desired version of the API.
404     #
405     # @return [Google::APIClient::Method] The method object.
406     def discovered_method(rpc_name, api, version=nil)
407       if !rpc_name.kind_of?(String) && !rpc_name.kind_of?(Symbol)
408         raise TypeError,
409           "Expected String or Symbol, got #{rpc_name.class}."
410       end
411       rpc_name = rpc_name.to_s
412       api = api.to_s
413       version = version || 'v1'
414       service = self.discovered_api(api, version)
415       if service.to_h[rpc_name]
416         return service.to_h[rpc_name]
417       else
418         return nil
419       end
420     end
421
422     ##
423     # Returns the service object with the highest version number.
424     #
425     # @note <em>Warning</em>: This method should be used with great care.
426     # As APIs are updated, minor differences between versions may cause
427     # incompatibilities. Requesting a specific version will avoid this issue.
428     #
429     # @param [String, Symbol] api The name of the service.
430     #
431     # @return [Google::APIClient::API] The service object.
432     def preferred_version(api)
433       if !api.kind_of?(String) && !api.kind_of?(Symbol)
434         raise TypeError,
435           "Expected String or Symbol, got #{api.class}."
436       end
437       api = api.to_s
438       return self.discovered_apis.detect do |a|
439         a.name == api && a.preferred == true
440       end
441     end
442
443     ##
444     # Verifies an ID token against a server certificate. Used to ensure that
445     # an ID token supplied by an untrusted client-side mechanism is valid.
446     # Raises an error if the token is invalid or missing.
447     # 
448     # @deprecated Use the google-id-token gem for verifying JWTs
449     def verify_id_token!
450       require 'jwt'
451       require 'openssl'
452       @certificates ||= {}
453       if !self.authorization.respond_to?(:id_token)
454         raise ArgumentError, (
455           "Current authorization mechanism does not support ID tokens: " +
456           "#{self.authorization.class.to_s}"
457         )
458       elsif !self.authorization.id_token
459         raise ArgumentError, (
460           "Could not verify ID token, ID token missing. " +
461           "Scopes were: #{self.authorization.scope.inspect}"
462         )
463       else
464         check_cached_certs = lambda do
465           valid = false
466           for key, cert in @certificates
467             begin
468               self.authorization.decoded_id_token(cert.public_key)
469               valid = true
470             rescue JWT::DecodeError, Signet::UnsafeOperationError
471               # Expected exception. Ignore, ID token has not been validated.
472             end
473           end
474           valid
475         end
476         if check_cached_certs.call()
477           return true
478         end
479         response = self.execute!(
480           :http_method => :get,
481           :uri => 'https://www.googleapis.com/oauth2/v1/certs',
482           :authenticated => false
483         )
484         @certificates.merge!(
485           Hash[MultiJson.load(response.body).map do |key, cert|
486             [key, OpenSSL::X509::Certificate.new(cert)]
487           end]
488         )
489         if check_cached_certs.call()
490           return true
491         else
492           raise InvalidIDTokenError,
493             "Could not verify ID token against any available certificate."
494         end
495       end
496       return nil
497     end
498
499     ##
500     # Generates a request.
501     #
502     # @option options [Google::APIClient::Method] :api_method
503     #   The method object or the RPC name of the method being executed.
504     # @option options [Hash, Array] :parameters
505     #   The parameters to send to the method.
506     # @option options [Hash, Array] :headers The HTTP headers for the request.
507     # @option options [String] :body The body of the request.
508     # @option options [String] :version ("v1")
509     #   The service version. Only used if `api_method` is a `String`.
510     # @option options [#generate_authenticated_request] :authorization
511     #   The authorization mechanism for the response. Used only if
512     #   `:authenticated` is `true`.
513     # @option options [TrueClass, FalseClass] :authenticated (true)
514     #   `true` if the request must be signed or somehow
515     #   authenticated, `false` otherwise.
516     #
517     # @return [Google::APIClient::Reference] The generated request.
518     #
519     # @example
520     #   request = client.generate_request(
521     #     :api_method => 'plus.activities.list',
522     #     :parameters =>
523     #       {'collection' => 'public', 'userId' => 'me'}
524     #   )
525     def generate_request(options={})
526       options = {
527         :api_client => self
528       }.merge(options)
529       return Google::APIClient::Request.new(options)
530     end
531
532     ##
533     # Executes a request, wrapping it in a Result object.
534     #
535     # @param [Google::APIClient::Request, Hash, Array] params
536     #   Either a Google::APIClient::Request, a Hash, or an Array.
537     #
538     #   If a Google::APIClient::Request, no other parameters are expected.
539     #
540     #   If a Hash, the below parameters are handled. If an Array, the
541     #   parameters are assumed to be in the below order:
542     #
543     #   - (Google::APIClient::Method) api_method:
544     #     The method object or the RPC name of the method being executed.
545     #   - (Hash, Array) parameters:
546     #     The parameters to send to the method.
547     #   - (String) body: The body of the request.
548     #   - (Hash, Array) headers: The HTTP headers for the request.
549     #   - (Hash) options: A set of options for the request, of which:
550     #     - (#generate_authenticated_request) :authorization (default: true) -
551     #       The authorization mechanism for the response. Used only if
552     #       `:authenticated` is `true`.
553     #     - (TrueClass, FalseClass) :authenticated (default: true) -
554     #       `true` if the request must be signed or somehow
555     #       authenticated, `false` otherwise.
556     #     - (TrueClass, FalseClass) :gzip (default: true) - 
557     #       `true` if gzip enabled, `false` otherwise.
558     #     - (FixNum) :retries -
559     #       # of times to retry on recoverable errors
560     #
561     # @return [Google::APIClient::Result] The result from the API, nil if batch.
562     #
563     # @example
564     #   result = client.execute(batch_request)
565     #
566     # @example
567     #   plus = client.discovered_api('plus')
568     #   result = client.execute(
569     #     :api_method => plus.activities.list,
570     #     :parameters => {'collection' => 'public', 'userId' => 'me'}
571     #   )
572     #
573     # @see Google::APIClient#generate_request
574     def execute!(*params)
575       if params.first.kind_of?(Google::APIClient::Request)
576         request = params.shift
577         options = params.shift || {}
578       else
579         # This block of code allows us to accept multiple parameter passing
580         # styles, and maintaining some backwards compatibility.
581         #
582         # Note: I'm extremely tempted to deprecate this style of execute call.
583         if params.last.respond_to?(:to_hash) && params.size == 1
584           options = params.pop
585         else
586           options = {}
587         end
588
589         options[:api_method] = params.shift if params.size > 0
590         options[:parameters] = params.shift if params.size > 0
591         options[:body] = params.shift if params.size > 0
592         options[:headers] = params.shift if params.size > 0
593         options.update(params.shift) if params.size > 0
594         request = self.generate_request(options)
595       end
596       
597       request.headers['User-Agent'] ||= '' + self.user_agent unless self.user_agent.nil?
598       request.headers['Accept-Encoding'] ||= 'gzip' unless options[:gzip] == false
599       request.headers['Content-Type'] ||= ''
600       request.parameters['key'] ||= self.key unless self.key.nil?
601       request.parameters['userIp'] ||= self.user_ip unless self.user_ip.nil?
602
603       connection = options[:connection] || self.connection
604       request.authorization = options[:authorization] || self.authorization unless options[:authenticated] == false
605       
606       tries = 1 + (options[:retries] || self.retries)
607
608       Retriable.retriable :tries => tries, 
609                           :on => [TransmissionError],
610                           :on_retry => client_error_handler(request.authorization),
611                           :interval => lambda {|attempts| (2 ** attempts) + rand} do
612         result = request.send(connection, true)
613
614         case result.status
615           when 200...300
616             result
617           when 301, 302, 303, 307
618             request = generate_request(request.to_hash.merge({
619               :uri => result.headers['location'],
620               :api_method => nil
621             }))
622             raise RedirectError.new(result.headers['location'], result)
623           when 400...500
624             raise ClientError.new(result.error_message || "A client error has occurred", result)
625           when 500...600
626             raise ServerError.new(result.error_message || "A server error has occurred", result)
627           else
628             raise TransmissionError.new(result.error_message || "A transmission error has occurred", result)
629         end
630       end
631     end
632
633     ##
634     # Same as Google::APIClient#execute!, but does not raise an exception for
635     # normal API errros.
636     #
637     # @see Google::APIClient#execute
638     def execute(*params)
639       begin
640         return self.execute!(*params)
641       rescue TransmissionError => e
642         return e.result
643       end
644     end
645
646     protected
647
648     ##
649     # Resolves a URI template against the client's configured base.
650     #
651     # @api private
652     # @param [String, Addressable::URI, Addressable::Template] template
653     #   The template to resolve.
654     # @param [Hash] mapping The mapping that corresponds to the template.
655     # @return [Addressable::URI] The expanded URI.
656     def resolve_uri(template, mapping={})
657       @base_uri ||= Addressable::URI.new(
658         :scheme => 'https',
659         :host => self.host,
660         :port => self.port
661       ).normalize
662       template = if template.kind_of?(Addressable::Template)
663         template.pattern
664       elsif template.respond_to?(:to_str)
665         template.to_str
666       else
667         raise TypeError,
668           "Expected String, Addressable::URI, or Addressable::Template, " +
669           "got #{template.class}."
670       end
671       return Addressable::Template.new(@base_uri + template).expand(mapping)
672     end
673     
674
675     ##
676     # Returns on proc for special processing of retries as not all client errors
677     # are recoverable. Only 401s should be retried and only if the credentials
678     # are refreshable
679     #
680     # @param [#fetch_access_token!] authorization
681     #   OAuth 2 credentials
682     # @return [Proc] 
683     def client_error_handler(authorization)  
684       can_refresh = authorization.respond_to?(:refresh_token) && auto_refresh_token 
685       Proc.new do |exception, tries|
686         next unless exception.kind_of?(ClientError)
687         if exception.result.status == 401 && can_refresh && tries == 1
688           begin
689             logger.debug("Attempting refresh of access token & retry of request")
690             authorization.fetch_access_token!
691             next
692           rescue Signet::AuthorizationError
693           end
694         end
695         raise exception
696       end
697     end
698
699   end
700
701 end