googleauth 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +36 -0
- data/.rspec +2 -0
- data/.travis.yml +18 -0
- data/CONTRIBUTING.md +32 -0
- data/COPYING +202 -0
- data/Gemfile +4 -0
- data/README.md +90 -0
- data/Rakefile +15 -0
- data/googleauth.gemspec +39 -0
- data/lib/googleauth.rb +66 -0
- data/lib/googleauth/compute_engine.rb +86 -0
- data/lib/googleauth/service_account.rb +118 -0
- data/lib/googleauth/signet.rb +63 -0
- data/lib/googleauth/version.rb +36 -0
- data/spec/googleauth/apply_auth_examples.rb +169 -0
- data/spec/googleauth/compute_engine_spec.rb +108 -0
- data/spec/googleauth/get_application_default_spec.rb +134 -0
- data/spec/googleauth/service_account_spec.rb +143 -0
- data/spec/googleauth/signet_spec.rb +66 -0
- data/spec/spec_helper.rb +51 -0
- metadata +212 -0
data/lib/googleauth.rb
ADDED
@@ -0,0 +1,66 @@
|
|
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/service_account'
|
31
|
+
require 'googleauth/compute_engine'
|
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 = <<END
|
38
|
+
Could not load the default credentials. Browse to
|
39
|
+
https://developers.google.com/accounts/docs/application-default-credentials
|
40
|
+
for more information
|
41
|
+
END
|
42
|
+
|
43
|
+
# Obtains the default credentials implementation to use in this
|
44
|
+
# environment.
|
45
|
+
#
|
46
|
+
# Use this to obtain the Application Default Credentials for accessing
|
47
|
+
# Google APIs. Application Default Credentials are described in detail
|
48
|
+
# at http://goo.gl/IUuyuX.
|
49
|
+
#
|
50
|
+
# If supplied, scope is used to create the credentials instance, when it
|
51
|
+
# can applied. E.g, on compute engine, the scope is ignored.
|
52
|
+
#
|
53
|
+
# @param scope [string|array] the scope(s) to access
|
54
|
+
# @param options [hash] allows override of the connection being used
|
55
|
+
def get_application_default(scope, options = {})
|
56
|
+
creds = ServiceAccountCredentials.from_env(scope)
|
57
|
+
return creds unless creds.nil?
|
58
|
+
creds = ServiceAccountCredentials.from_well_known_path(scope)
|
59
|
+
return creds unless creds.nil?
|
60
|
+
fail NOT_FOUND_ERROR unless GCECredentials.on_gce?(options)
|
61
|
+
GCECredentials.new
|
62
|
+
end
|
63
|
+
|
64
|
+
module_function :get_application_default
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,86 @@
|
|
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 'faraday'
|
31
|
+
require 'googleauth/signet'
|
32
|
+
require 'memoist'
|
33
|
+
|
34
|
+
module Google
|
35
|
+
# Module Auth provides classes that provide Google-specific authorization
|
36
|
+
# used to access Google APIs.
|
37
|
+
module Auth
|
38
|
+
# Extends Signet::OAuth2::Client so that the auth token is obtained from
|
39
|
+
# the GCE metadata server.
|
40
|
+
class GCECredentials < Signet::OAuth2::Client
|
41
|
+
# The IP Address is used in the URIs to speed up failures on non-GCE
|
42
|
+
# 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'
|
46
|
+
|
47
|
+
class << self
|
48
|
+
extend Memoist
|
49
|
+
|
50
|
+
# Detect if this appear to be a GCE instance, by checking if metadata
|
51
|
+
# is available
|
52
|
+
def on_gce?(options = {})
|
53
|
+
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
|
64
|
+
end
|
65
|
+
return false unless resp.status == 200
|
66
|
+
return false unless resp.headers.key?('Metadata-Flavor')
|
67
|
+
return resp.headers['Metadata-Flavor'] == 'Google'
|
68
|
+
rescue Faraday::TimeoutError, Faraday::ConnectionFailed
|
69
|
+
return false
|
70
|
+
end
|
71
|
+
|
72
|
+
memoize :on_gce?
|
73
|
+
end
|
74
|
+
|
75
|
+
# Overrides the super class method to change how access tokens are
|
76
|
+
# fetched.
|
77
|
+
def fetch_access_token(options = {})
|
78
|
+
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'])
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,118 @@
|
|
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/signet'
|
31
|
+
require 'memoist'
|
32
|
+
require 'multi_json'
|
33
|
+
require 'openssl'
|
34
|
+
require 'rbconfig'
|
35
|
+
|
36
|
+
# Reads the private key and client email fields from service account JSON key.
|
37
|
+
def read_json_key(json_key_io)
|
38
|
+
json_key = MultiJson.load(json_key_io.read)
|
39
|
+
fail 'missing client_email' unless json_key.key?('client_email')
|
40
|
+
fail 'missing private_key' unless json_key.key?('private_key')
|
41
|
+
[json_key['private_key'], json_key['client_email']]
|
42
|
+
end
|
43
|
+
|
44
|
+
module Google
|
45
|
+
# Module Auth provides classes that provide Google-specific authorization
|
46
|
+
# used to access Google APIs.
|
47
|
+
module Auth
|
48
|
+
# Authenticates requests using Google's Service Account credentials.
|
49
|
+
#
|
50
|
+
# This class allows authorizing requests for service accounts directly
|
51
|
+
# from credentials from a json key file downloaded from the developer
|
52
|
+
# console (via 'Generate new Json Key').
|
53
|
+
#
|
54
|
+
# cf [Application Default Credentials](http://goo.gl/mkAHpZ)
|
55
|
+
class ServiceAccountCredentials < Signet::OAuth2::Client
|
56
|
+
ENV_VAR = 'GOOGLE_APPLICATION_CREDENTIALS'
|
57
|
+
NOT_FOUND_ERROR =
|
58
|
+
"Unable to read the credential file specified by #{ENV_VAR}"
|
59
|
+
TOKEN_CRED_URI = 'https://www.googleapis.com/oauth2/v3/token'
|
60
|
+
WELL_KNOWN_PATH = 'gcloud/application_default_credentials.json'
|
61
|
+
WELL_KNOWN_ERROR = 'Unable to read the default credential file'
|
62
|
+
|
63
|
+
class << self
|
64
|
+
extend Memoist
|
65
|
+
|
66
|
+
# determines if the current OS is windows
|
67
|
+
def windows?
|
68
|
+
RbConfig::CONFIG['host_os'] =~ /Windows|mswin/
|
69
|
+
end
|
70
|
+
memoize :windows?
|
71
|
+
|
72
|
+
# Creates an instance from the path specified in an environment
|
73
|
+
# variable.
|
74
|
+
#
|
75
|
+
# @param scope [string|array] the scope(s) to access
|
76
|
+
def from_env(scope)
|
77
|
+
return nil unless ENV.key?(ENV_VAR)
|
78
|
+
path = ENV[ENV_VAR]
|
79
|
+
fail 'file #{path} does not exist' unless File.exist?(path)
|
80
|
+
File.open(path) do |f|
|
81
|
+
return new(scope, f)
|
82
|
+
end
|
83
|
+
rescue StandardError => e
|
84
|
+
raise "#{NOT_FOUND_ERROR}: #{e}"
|
85
|
+
end
|
86
|
+
|
87
|
+
# Creates an instance from a well known path.
|
88
|
+
#
|
89
|
+
# @param scope [string|array] the scope(s) to access
|
90
|
+
def from_well_known_path(scope)
|
91
|
+
home_var, base = windows? ? 'APPDATA' : 'HOME', WELL_KNOWN_PATH
|
92
|
+
root = ENV[home_var].nil? ? '' : ENV[home_var]
|
93
|
+
base = File.join('.config', base) unless windows?
|
94
|
+
path = File.join(root, base)
|
95
|
+
return nil unless File.exist?(path)
|
96
|
+
File.open(path) do |f|
|
97
|
+
return new(scope, f)
|
98
|
+
end
|
99
|
+
rescue StandardError => e
|
100
|
+
raise "#{WELL_KNOWN_ERROR}: #{e}"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Initializes a ServiceAccountCredentials.
|
105
|
+
#
|
106
|
+
# @param scope [string|array] the scope(s) to access
|
107
|
+
# @param json_key_io [IO] an IO from which the JSON key can be read
|
108
|
+
def initialize(scope, json_key_io)
|
109
|
+
private_key, client_email = read_json_key(json_key_io)
|
110
|
+
super(token_credential_uri: TOKEN_CRED_URI,
|
111
|
+
audience: TOKEN_CRED_URI, # TODO: confirm this
|
112
|
+
scope: scope,
|
113
|
+
issuer: client_email,
|
114
|
+
signing_key: OpenSSL::PKey::RSA.new(private_key))
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,63 @@
|
|
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 'signet/oauth_2/client'
|
31
|
+
|
32
|
+
module Signet
|
33
|
+
# OAuth2 supports OAuth2 authentication.
|
34
|
+
module OAuth2
|
35
|
+
AUTH_METADATA_KEY = :Authorization
|
36
|
+
# Signet::OAuth2::Client creates an OAuth2 client
|
37
|
+
#
|
38
|
+
# This reopens Client to add #apply and #apply! methods which update a
|
39
|
+
# hash with the fetched authentication token.
|
40
|
+
class Client
|
41
|
+
# Updates a_hash updated with the authentication token
|
42
|
+
def apply!(a_hash, opts = {})
|
43
|
+
# fetch the access token there is currently not one, or if the client
|
44
|
+
# has expired
|
45
|
+
fetch_access_token!(opts) if access_token.nil? || expired?
|
46
|
+
a_hash[AUTH_METADATA_KEY] = "Bearer #{access_token}"
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns a clone of a_hash updated with the authentication token
|
50
|
+
def apply(a_hash, opts = {})
|
51
|
+
a_copy = a_hash.clone
|
52
|
+
apply!(a_copy, opts)
|
53
|
+
a_copy
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns a reference to the #apply method, suitable for passing as
|
57
|
+
# a closure
|
58
|
+
def updater_proc
|
59
|
+
lambda(&method(:apply))
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,36 @@
|
|
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
|
+
module Google
|
31
|
+
# Module Auth provides classes that provide Google-specific authorization
|
32
|
+
# used to access Google APIs.
|
33
|
+
module Auth
|
34
|
+
VERSION = '0.1.0'
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,169 @@
|
|
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
|
+
spec_dir = File.expand_path(File.join(File.dirname(__FILE__)))
|
31
|
+
$LOAD_PATH.unshift(spec_dir)
|
32
|
+
$LOAD_PATH.uniq!
|
33
|
+
|
34
|
+
require 'faraday'
|
35
|
+
require 'spec_helper'
|
36
|
+
|
37
|
+
def build_json_response(payload)
|
38
|
+
[200,
|
39
|
+
{ 'Content-Type' => 'application/json; charset=utf-8' },
|
40
|
+
MultiJson.dump(payload)]
|
41
|
+
end
|
42
|
+
|
43
|
+
def build_access_token_json(token)
|
44
|
+
build_json_response('access_token' => token,
|
45
|
+
'token_type' => 'Bearer',
|
46
|
+
'expires_in' => 3600)
|
47
|
+
end
|
48
|
+
|
49
|
+
WANTED_AUTH_KEY = :Authorization
|
50
|
+
|
51
|
+
shared_examples 'apply/apply! are OK' do
|
52
|
+
# tests that use these examples need to define
|
53
|
+
#
|
54
|
+
# @client which should be an auth client
|
55
|
+
#
|
56
|
+
# @make_auth_stubs, which should stub out the expected http behaviour of the
|
57
|
+
# auth client
|
58
|
+
describe '#fetch_access_token' do
|
59
|
+
it 'should set access_token to the fetched value' do
|
60
|
+
token = '1/abcdef1234567890'
|
61
|
+
stubs = make_auth_stubs access_token: token
|
62
|
+
c = Faraday.new do |b|
|
63
|
+
b.adapter(:test, stubs)
|
64
|
+
end
|
65
|
+
|
66
|
+
@client.fetch_access_token!(connection: c)
|
67
|
+
expect(@client.access_token).to eq(token)
|
68
|
+
stubs.verify_stubbed_calls
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe '#apply!' do
|
73
|
+
it 'should update the target hash with fetched access token' do
|
74
|
+
token = '1/abcdef1234567890'
|
75
|
+
stubs = make_auth_stubs access_token: token
|
76
|
+
c = Faraday.new do |b|
|
77
|
+
b.adapter(:test, stubs)
|
78
|
+
end
|
79
|
+
|
80
|
+
md = { foo: 'bar' }
|
81
|
+
@client.apply!(md, connection: c)
|
82
|
+
want = { :foo => 'bar', WANTED_AUTH_KEY => "Bearer #{token}" }
|
83
|
+
expect(md).to eq(want)
|
84
|
+
stubs.verify_stubbed_calls
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
describe 'updater_proc' do
|
89
|
+
it 'should provide a proc that updates a hash with the access token' do
|
90
|
+
token = '1/abcdef1234567890'
|
91
|
+
stubs = make_auth_stubs access_token: token
|
92
|
+
c = Faraday.new do |b|
|
93
|
+
b.adapter(:test, stubs)
|
94
|
+
end
|
95
|
+
|
96
|
+
md = { foo: 'bar' }
|
97
|
+
the_proc = @client.updater_proc
|
98
|
+
got = the_proc.call(md, connection: c)
|
99
|
+
want = { :foo => 'bar', WANTED_AUTH_KEY => "Bearer #{token}" }
|
100
|
+
expect(got).to eq(want)
|
101
|
+
stubs.verify_stubbed_calls
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
describe '#apply' do
|
106
|
+
it 'should not update the original hash with the access token' do
|
107
|
+
token = '1/abcdef1234567890'
|
108
|
+
stubs = make_auth_stubs access_token: token
|
109
|
+
c = Faraday.new do |b|
|
110
|
+
b.adapter(:test, stubs)
|
111
|
+
end
|
112
|
+
|
113
|
+
md = { foo: 'bar' }
|
114
|
+
@client.apply(md, connection: c)
|
115
|
+
want = { foo: 'bar' }
|
116
|
+
expect(md).to eq(want)
|
117
|
+
stubs.verify_stubbed_calls
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'should add the token to the returned hash' do
|
121
|
+
token = '1/abcdef1234567890'
|
122
|
+
stubs = make_auth_stubs access_token: token
|
123
|
+
c = Faraday.new do |b|
|
124
|
+
b.adapter(:test, stubs)
|
125
|
+
end
|
126
|
+
|
127
|
+
md = { foo: 'bar' }
|
128
|
+
got = @client.apply(md, connection: c)
|
129
|
+
want = { :foo => 'bar', WANTED_AUTH_KEY => "Bearer #{token}" }
|
130
|
+
expect(got).to eq(want)
|
131
|
+
stubs.verify_stubbed_calls
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'should not fetch a new token if the current is not expired' do
|
135
|
+
token = '1/abcdef1234567890'
|
136
|
+
stubs = make_auth_stubs access_token: token
|
137
|
+
c = Faraday.new do |b|
|
138
|
+
b.adapter(:test, stubs)
|
139
|
+
end
|
140
|
+
|
141
|
+
n = 5 # arbitrary
|
142
|
+
n.times do |_t|
|
143
|
+
md = { foo: 'bar' }
|
144
|
+
got = @client.apply(md, connection: c)
|
145
|
+
want = { :foo => 'bar', WANTED_AUTH_KEY => "Bearer #{token}" }
|
146
|
+
expect(got).to eq(want)
|
147
|
+
end
|
148
|
+
stubs.verify_stubbed_calls
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'should fetch a new token if the current one is expired' do
|
152
|
+
token_1 = '1/abcdef1234567890'
|
153
|
+
token_2 = '2/abcdef1234567890'
|
154
|
+
|
155
|
+
[token_1, token_2].each do |t|
|
156
|
+
stubs = make_auth_stubs access_token: t
|
157
|
+
c = Faraday.new do |b|
|
158
|
+
b.adapter(:test, stubs)
|
159
|
+
end
|
160
|
+
md = { foo: 'bar' }
|
161
|
+
got = @client.apply(md, connection: c)
|
162
|
+
want = { :foo => 'bar', WANTED_AUTH_KEY => "Bearer #{t}" }
|
163
|
+
expect(got).to eq(want)
|
164
|
+
stubs.verify_stubbed_calls
|
165
|
+
@client.expires_at -= 3601 # default is to expire in 1hr
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|