googleauth 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0585a45f95885a97c727eef608c4ed257676f1a7
4
- data.tar.gz: e7caca78b8212296986474b7d1c6eab7f3a4666b
3
+ metadata.gz: 85200987eca465d01dd0eab971906ba8ee145ca4
4
+ data.tar.gz: 78773e93e52dabae41d49e86c602e3352ca96b6c
5
5
  SHA512:
6
- metadata.gz: ac7fc0cc2b64b76aa51ff9e0f2c26c4432624ebc8b1c8ac7b47022448c7d946f3071e858cbc20c6671309cd966ae2e59a46f2855c6b78f5a435607637859070e
7
- data.tar.gz: 362554243c77276cf8c9f560117cd8627971c957af4ab1bac886eafec19482437bcca6fb034ac84609aa09321629f8cccb1ba26693f81881c1ea24ebc364e58e
6
+ metadata.gz: 9e06dde72a93f223edccfc50824ed9e6b1c46843ba0f4afc0aed0882670637161377503ced93b660b602fa053e9e1c2d243e0f313f0f843086352fd2ca0acc45
7
+ data.tar.gz: 30e4767d112096c8916f78a28bc4d48574c21239bd3dcc052a702fd4ab65b490f85189c2313ea7a416d771ce15c3f5449935d89788077a3df66cd595ff77756b
@@ -1,15 +1,15 @@
1
1
  # This configuration was generated by `rubocop --auto-gen-config`
2
- # on 2015-02-25 04:34:33 -0800 using RuboCop version 0.28.0.
2
+ # on 2015-03-06 19:51:00 -0800 using RuboCop version 0.28.0.
3
3
  # The point is for the user to remove these configuration records
4
4
  # one by one as the offenses are removed from the code base.
5
5
  # Note that changes in the inspected code, or installation of new
6
6
  # versions of RuboCop, may require this file to be generated again.
7
7
 
8
- # Offense count: 1
8
+ # Offense count: 3
9
9
  Metrics/AbcSize:
10
- Max: 16
10
+ Max: 24
11
11
 
12
- # Offense count: 1
12
+ # Offense count: 3
13
13
  # Configuration parameters: CountComments.
14
14
  Metrics/MethodLength:
15
15
  Max: 11
@@ -1,3 +1,9 @@
1
+ ## 0.4.0 (25/03/2015)
2
+
3
+ ### Changes
4
+
5
+ * Adds an implementation of JWT header auth ([@tbetbetbe][])
6
+
1
7
  ## 0.3.0 (23/03/2015)
2
8
 
3
9
  ### Changes
@@ -27,15 +27,15 @@ Gem::Specification.new do |s|
27
27
 
28
28
  s.add_dependency 'faraday', '~> 0.9'
29
29
  s.add_dependency 'logging', '~> 1.8'
30
- s.add_dependency 'jwt', '~> 1.4.1'
31
- s.add_dependency 'memoist', '~> 0.11.0'
32
- s.add_dependency 'multi_json', '1.11.0'
33
- s.add_dependency 'signet', '~> 0.6.0'
30
+ s.add_dependency 'jwt', '~> 1.4'
31
+ s.add_dependency 'memoist', '~> 0.11'
32
+ s.add_dependency 'multi_json', '1.11'
33
+ s.add_dependency 'signet', '~> 0.6'
34
34
 
35
35
  s.add_development_dependency 'bundler', '~> 1.7'
36
- s.add_development_dependency 'simplecov', '~> 0.9.2'
37
- s.add_development_dependency 'coveralls', '~> 0.7.11'
36
+ s.add_development_dependency 'simplecov', '~> 0.9'
37
+ s.add_development_dependency 'coveralls', '~> 0.7'
38
38
  s.add_development_dependency 'rake', '~> 10.0'
39
- s.add_development_dependency 'rubocop', '~> 0.29.1'
39
+ s.add_development_dependency 'rubocop', '~> 0.29'
40
40
  s.add_development_dependency 'rspec', '~> 3.0'
41
41
  end
@@ -29,13 +29,16 @@
29
29
 
30
30
  require 'googleauth/signet'
31
31
  require 'googleauth/credentials_loader'
32
+ require 'jwt'
32
33
  require 'multi_json'
34
+ require 'stringio'
33
35
 
34
36
  module Google
35
37
  # Module Auth provides classes that provide Google-specific authorization
36
38
  # used to access Google APIs.
37
39
  module Auth
38
- # Authenticates requests using Google's Service Account credentials.
40
+ # Authenticates requests using Google's Service Account credentials via an
41
+ # OAuth access token.
39
42
  #
40
43
  # This class allows authorizing requests for service accounts directly
41
44
  # from credentials from a json key file downloaded from the developer
@@ -67,6 +70,118 @@ module Google
67
70
  issuer: client_email,
68
71
  signing_key: OpenSSL::PKey::RSA.new(private_key))
69
72
  end
73
+
74
+ # Extends the base class.
75
+ #
76
+ # If scope(s) is not set, it creates a transient
77
+ # ServiceAccountJwtHeaderCredentials instance and uses that to
78
+ # authenticate instead.
79
+ def apply!(a_hash, opts = {})
80
+ # Use the base implementation if scopes are set
81
+ unless scope.nil?
82
+ super
83
+ return
84
+ end
85
+
86
+ # Use the ServiceAccountJwtHeaderCredentials using the same cred values
87
+ # if no scopes are set.
88
+ cred_json = {
89
+ private_key: @signing_key.to_s,
90
+ client_email: @issuer
91
+ }
92
+ alt_clz = ServiceAccountJwtHeaderCredentials
93
+ alt = alt_clz.new(StringIO.new(MultiJson.dump(cred_json)))
94
+ alt.apply!(a_hash)
95
+ end
96
+ end
97
+
98
+ # Authenticates requests using Google's Service Account credentials via
99
+ # JWT Header.
100
+ #
101
+ # This class allows authorizing requests for service accounts directly
102
+ # from credentials from a json key file downloaded from the developer
103
+ # console (via 'Generate new Json Key'). It is not part of any OAuth2
104
+ # flow, rather it creates a JWT and sends that as a credential.
105
+ #
106
+ # cf [Application Default Credentials](http://goo.gl/mkAHpZ)
107
+ class ServiceAccountJwtHeaderCredentials
108
+ JWT_AUD_URI_KEY = :jwt_aud_uri
109
+ AUTH_METADATA_KEY = Signet::OAuth2::AUTH_METADATA_KEY
110
+ TOKEN_CRED_URI = 'https://www.googleapis.com/oauth2/v3/token'
111
+ SIGNING_ALGORITHM = 'RS256'
112
+ EXPIRY = 60
113
+ extend CredentialsLoader
114
+
115
+ # make_creds proxies the construction of a credentials instance
116
+ #
117
+ # make_creds is used by the methods in CredentialsLoader.
118
+ #
119
+ # By default, it calls #new with 2 args, the second one being an
120
+ # optional scope. Here's the constructor only has one param, so
121
+ # we modify make_creds to reflect this.
122
+ def self.make_creds(*args)
123
+ new(args[0])
124
+ end
125
+
126
+ # Reads the private key and client email fields from the service account
127
+ # JSON key.
128
+ def self.read_json_key(json_key_io)
129
+ json_key = MultiJson.load(json_key_io.read)
130
+ fail 'missing client_email' unless json_key.key?('client_email')
131
+ fail 'missing private_key' unless json_key.key?('private_key')
132
+ [json_key['private_key'], json_key['client_email']]
133
+ end
134
+
135
+ # Initializes a ServiceAccountJwtHeaderCredentials.
136
+ #
137
+ # @param json_key_io [IO] an IO from which the JSON key can be read
138
+ def initialize(json_key_io)
139
+ private_key, client_email = self.class.read_json_key(json_key_io)
140
+ @private_key = private_key
141
+ @issuer = client_email
142
+ @signing_key = OpenSSL::PKey::RSA.new(private_key)
143
+ end
144
+
145
+ # Construct a jwt token if the JWT_AUD_URI key is present in the input
146
+ # hash.
147
+ #
148
+ # The jwt token is used as the value of a 'Bearer '.
149
+ def apply!(a_hash, opts = {})
150
+ jwt_aud_uri = a_hash.delete(JWT_AUD_URI_KEY)
151
+ return a_hash if jwt_aud_uri.nil?
152
+ jwt_token = new_jwt_token(jwt_aud_uri, opts)
153
+ a_hash[AUTH_METADATA_KEY] = "Bearer #{jwt_token}"
154
+ a_hash
155
+ end
156
+
157
+ # Returns a clone of a_hash updated with the authoriation header
158
+ def apply(a_hash, opts = {})
159
+ a_copy = a_hash.clone
160
+ apply!(a_copy, opts)
161
+ a_copy
162
+ end
163
+
164
+ # Returns a reference to the #apply method, suitable for passing as
165
+ # a closure
166
+ def updater_proc
167
+ lambda(&method(:apply))
168
+ end
169
+
170
+ protected
171
+
172
+ # Creates a jwt uri token.
173
+ def new_jwt_token(jwt_aud_uri, options = {})
174
+ now = Time.new
175
+ skew = options[:skew] || 60
176
+ assertion = {
177
+ 'iss' => @issuer,
178
+ 'sub' => @issuer,
179
+ 'aud' => jwt_aud_uri,
180
+ 'exp' => (now + EXPIRY).to_i,
181
+ 'iat' => (now - skew).to_i
182
+ }
183
+ JWT.encode(assertion, @signing_key, SIGNING_ALGORITHM)
184
+ end
70
185
  end
71
186
  end
72
187
  end
@@ -31,6 +31,6 @@ module Google
31
31
  # Module Auth provides classes that provide Google-specific authorization
32
32
  # used to access Google APIs.
33
33
  module Auth
34
- VERSION = '0.3.0'
34
+ VERSION = '0.4.0'
35
35
  end
36
36
  end
@@ -46,9 +46,9 @@ def build_access_token_json(token)
46
46
  'expires_in' => 3600)
47
47
  end
48
48
 
49
- WANTED_AUTH_KEY = :Authorization
50
-
51
49
  shared_examples 'apply/apply! are OK' do
50
+ let(:auth_key) { :Authorization }
51
+
52
52
  # tests that use these examples need to define
53
53
  #
54
54
  # @client which should be an auth client
@@ -79,7 +79,7 @@ shared_examples 'apply/apply! are OK' do
79
79
 
80
80
  md = { foo: 'bar' }
81
81
  @client.apply!(md, connection: c)
82
- want = { :foo => 'bar', WANTED_AUTH_KEY => "Bearer #{token}" }
82
+ want = { :foo => 'bar', auth_key => "Bearer #{token}" }
83
83
  expect(md).to eq(want)
84
84
  stubs.verify_stubbed_calls
85
85
  end
@@ -96,7 +96,7 @@ shared_examples 'apply/apply! are OK' do
96
96
  md = { foo: 'bar' }
97
97
  the_proc = @client.updater_proc
98
98
  got = the_proc.call(md, connection: c)
99
- want = { :foo => 'bar', WANTED_AUTH_KEY => "Bearer #{token}" }
99
+ want = { :foo => 'bar', auth_key => "Bearer #{token}" }
100
100
  expect(got).to eq(want)
101
101
  stubs.verify_stubbed_calls
102
102
  end
@@ -126,7 +126,7 @@ shared_examples 'apply/apply! are OK' do
126
126
 
127
127
  md = { foo: 'bar' }
128
128
  got = @client.apply(md, connection: c)
129
- want = { :foo => 'bar', WANTED_AUTH_KEY => "Bearer #{token}" }
129
+ want = { :foo => 'bar', auth_key => "Bearer #{token}" }
130
130
  expect(got).to eq(want)
131
131
  stubs.verify_stubbed_calls
132
132
  end
@@ -142,7 +142,7 @@ shared_examples 'apply/apply! are OK' do
142
142
  n.times do |_t|
143
143
  md = { foo: 'bar' }
144
144
  got = @client.apply(md, connection: c)
145
- want = { :foo => 'bar', WANTED_AUTH_KEY => "Bearer #{token}" }
145
+ want = { :foo => 'bar', auth_key => "Bearer #{token}" }
146
146
  expect(got).to eq(want)
147
147
  end
148
148
  stubs.verify_stubbed_calls
@@ -159,7 +159,7 @@ shared_examples 'apply/apply! are OK' do
159
159
  end
160
160
  md = { foo: 'bar' }
161
161
  got = @client.apply(md, connection: c)
162
- want = { :foo => 'bar', WANTED_AUTH_KEY => "Bearer #{t}" }
162
+ want = { :foo => 'bar', auth_key => "Bearer #{t}" }
163
163
  expect(got).to eq(want)
164
164
  stubs.verify_stubbed_calls
165
165
  @client.expires_at -= 3601 # default is to expire in 1hr
@@ -40,9 +40,74 @@ require 'openssl'
40
40
  require 'spec_helper'
41
41
  require 'tmpdir'
42
42
 
43
+ include Google::Auth::CredentialsLoader
44
+
45
+ shared_examples 'jwt header auth' do
46
+ context 'when jwt_aud_uri is present' do
47
+ let(:test_uri) { 'https://www.googleapis.com/myservice' }
48
+ let(:auth_prefix) { 'Bearer ' }
49
+ let(:auth_key) { ServiceAccountJwtHeaderCredentials::AUTH_METADATA_KEY }
50
+ let(:jwt_uri_key) { ServiceAccountJwtHeaderCredentials::JWT_AUD_URI_KEY }
51
+
52
+ def expect_is_encoded_jwt(hdr)
53
+ expect(hdr).to_not be_nil
54
+ expect(hdr.start_with?(auth_prefix)).to be true
55
+ authorization = hdr[auth_prefix.length..-1]
56
+ payload, _ = JWT.decode(authorization, @key.public_key)
57
+ expect(payload['aud']).to eq(test_uri)
58
+ expect(payload['iss']).to eq(client_email)
59
+ end
60
+
61
+ describe '#apply!' do
62
+ it 'should update the target hash with a jwt token' do
63
+ md = { foo: 'bar' }
64
+ md[jwt_uri_key] = test_uri
65
+ @client.apply!(md)
66
+ auth_header = md[auth_key]
67
+ expect_is_encoded_jwt(auth_header)
68
+ expect(md[jwt_uri_key]).to be_nil
69
+ end
70
+ end
71
+
72
+ describe 'updater_proc' do
73
+ it 'should provide a proc that updates a hash with a jwt token' do
74
+ md = { foo: 'bar' }
75
+ md[jwt_uri_key] = test_uri
76
+ the_proc = @client.updater_proc
77
+ got = the_proc.call(md)
78
+ auth_header = got[auth_key]
79
+ expect_is_encoded_jwt(auth_header)
80
+ expect(got[jwt_uri_key]).to be_nil
81
+ expect(md[jwt_uri_key]).to_not be_nil
82
+ end
83
+ end
84
+
85
+ describe '#apply' do
86
+ it 'should not update the original hash with a jwt token' do
87
+ md = { foo: 'bar' }
88
+ md[jwt_uri_key] = test_uri
89
+ the_proc = @client.updater_proc
90
+ got = the_proc.call(md)
91
+ auth_header = md[auth_key]
92
+ expect(auth_header).to be_nil
93
+ expect(got[jwt_uri_key]).to be_nil
94
+ expect(md[jwt_uri_key]).to_not be_nil
95
+ end
96
+
97
+ it 'should add a jwt token to the returned hash' do
98
+ md = { foo: 'bar' }
99
+ md[jwt_uri_key] = test_uri
100
+ got = @client.apply(md)
101
+ auth_header = got[auth_key]
102
+ expect_is_encoded_jwt(auth_header)
103
+ end
104
+ end
105
+ end
106
+ end
107
+
43
108
  describe Google::Auth::ServiceAccountCredentials do
44
109
  ServiceAccountCredentials = Google::Auth::ServiceAccountCredentials
45
- CredentialsLoader = Google::Auth::CredentialsLoader
110
+ let(:client_email) { 'app@developer.gserviceaccount.com' }
46
111
 
47
112
  before(:example) do
48
113
  @key = OpenSSL::PKey::RSA.new(2048)
@@ -69,7 +134,7 @@ describe Google::Auth::ServiceAccountCredentials do
69
134
  cred_json = {
70
135
  private_key_id: 'a_private_key_id',
71
136
  private_key: @key.to_pem,
72
- client_email: 'app@developer.gserviceaccount.com',
137
+ client_email: client_email,
73
138
  client_id: 'app.apps.googleusercontent.com',
74
139
  type: 'service_account'
75
140
  }
@@ -78,9 +143,17 @@ describe Google::Auth::ServiceAccountCredentials do
78
143
 
79
144
  it_behaves_like 'apply/apply! are OK'
80
145
 
146
+ context 'when scope is nil' do
147
+ before(:example) do
148
+ @client.scope = nil
149
+ end
150
+
151
+ it_behaves_like 'jwt header auth'
152
+ end
153
+
81
154
  describe '#from_env' do
82
155
  before(:example) do
83
- @var_name = CredentialsLoader::ENV_VAR
156
+ @var_name = ENV_VAR
84
157
  @orig = ENV[@var_name]
85
158
  @scope = 'https://www.googleapis.com/auth/userinfo.profile'
86
159
  @clz = ServiceAccountCredentials
@@ -120,7 +193,7 @@ describe Google::Auth::ServiceAccountCredentials do
120
193
  before(:example) do
121
194
  @home = ENV['HOME']
122
195
  @scope = 'https://www.googleapis.com/auth/userinfo.profile'
123
- @known_path = CredentialsLoader::WELL_KNOWN_PATH
196
+ @known_path = WELL_KNOWN_PATH
124
197
  @clz = ServiceAccountCredentials
125
198
  end
126
199
 
@@ -144,3 +217,90 @@ describe Google::Auth::ServiceAccountCredentials do
144
217
  end
145
218
  end
146
219
  end
220
+
221
+ describe Google::Auth::ServiceAccountJwtHeaderCredentials do
222
+ ServiceAccountJwtHeaderCredentials =
223
+ Google::Auth::ServiceAccountJwtHeaderCredentials
224
+
225
+ let(:client_email) { 'app@developer.gserviceaccount.com' }
226
+ let(:clz) { Google::Auth::ServiceAccountJwtHeaderCredentials }
227
+
228
+ before(:example) do
229
+ @key = OpenSSL::PKey::RSA.new(2048)
230
+ @client = clz.new(StringIO.new(cred_json_text))
231
+ end
232
+
233
+ def cred_json_text
234
+ cred_json = {
235
+ private_key_id: 'a_private_key_id',
236
+ private_key: @key.to_pem,
237
+ client_email: client_email,
238
+ client_id: 'app.apps.googleusercontent.com',
239
+ type: 'service_account'
240
+ }
241
+ MultiJson.dump(cred_json)
242
+ end
243
+
244
+ it_behaves_like 'jwt header auth'
245
+
246
+ describe '#from_env' do
247
+ before(:example) do
248
+ @var_name = ENV_VAR
249
+ @orig = ENV[@var_name]
250
+ end
251
+
252
+ after(:example) do
253
+ ENV[@var_name] = @orig unless @orig.nil?
254
+ end
255
+
256
+ it 'returns nil if the GOOGLE_APPLICATION_CREDENTIALS is unset' do
257
+ ENV.delete(@var_name) unless ENV[@var_name].nil?
258
+ expect(clz.from_env).to be_nil
259
+ end
260
+
261
+ it 'fails if the GOOGLE_APPLICATION_CREDENTIALS path does not exist' do
262
+ ENV.delete(@var_name) unless ENV[@var_name].nil?
263
+ expect(clz.from_env).to be_nil
264
+ Dir.mktmpdir do |dir|
265
+ key_path = File.join(dir, 'does-not-exist')
266
+ ENV[@var_name] = key_path
267
+ expect { clz.from_env }.to raise_error
268
+ end
269
+ end
270
+
271
+ it 'succeeds when the GOOGLE_APPLICATION_CREDENTIALS file is valid' do
272
+ Dir.mktmpdir do |dir|
273
+ key_path = File.join(dir, 'my_cert_file')
274
+ FileUtils.mkdir_p(File.dirname(key_path))
275
+ File.write(key_path, cred_json_text)
276
+ ENV[@var_name] = key_path
277
+ expect(clz.from_env).to_not be_nil
278
+ end
279
+ end
280
+ end
281
+
282
+ describe '#from_well_known_path' do
283
+ before(:example) do
284
+ @home = ENV['HOME']
285
+ end
286
+
287
+ after(:example) do
288
+ ENV['HOME'] = @home unless @home == ENV['HOME']
289
+ end
290
+
291
+ it 'is nil if no file exists' do
292
+ ENV['HOME'] = File.dirname(__FILE__)
293
+ expect(clz.from_well_known_path).to be_nil
294
+ end
295
+
296
+ it 'successfully loads the file when it is present' do
297
+ Dir.mktmpdir do |dir|
298
+ key_path = File.join(dir, '.config', WELL_KNOWN_PATH)
299
+ FileUtils.mkdir_p(File.dirname(key_path))
300
+ File.write(key_path, cred_json_text)
301
+ ENV['HOME'] = dir
302
+ expect(clz.from_well_known_path).to_not be_nil
303
+ end
304
+ end
305
+ end
306
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: googleauth
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tim Emiola
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-03-24 00:00:00.000000000 Z
11
+ date: 2015-03-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -44,56 +44,56 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: 1.4.1
47
+ version: '1.4'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: 1.4.1
54
+ version: '1.4'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: memoist
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: 0.11.0
61
+ version: '0.11'
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: 0.11.0
68
+ version: '0.11'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: multi_json
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - '='
74
74
  - !ruby/object:Gem::Version
75
- version: 1.11.0
75
+ version: '1.11'
76
76
  type: :runtime
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - '='
81
81
  - !ruby/object:Gem::Version
82
- version: 1.11.0
82
+ version: '1.11'
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: signet
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: 0.6.0
89
+ version: '0.6'
90
90
  type: :runtime
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: 0.6.0
96
+ version: '0.6'
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: bundler
99
99
  requirement: !ruby/object:Gem::Requirement
@@ -114,28 +114,28 @@ dependencies:
114
114
  requirements:
115
115
  - - "~>"
116
116
  - !ruby/object:Gem::Version
117
- version: 0.9.2
117
+ version: '0.9'
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
122
  - - "~>"
123
123
  - !ruby/object:Gem::Version
124
- version: 0.9.2
124
+ version: '0.9'
125
125
  - !ruby/object:Gem::Dependency
126
126
  name: coveralls
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
129
  - - "~>"
130
130
  - !ruby/object:Gem::Version
131
- version: 0.7.11
131
+ version: '0.7'
132
132
  type: :development
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
136
  - - "~>"
137
137
  - !ruby/object:Gem::Version
138
- version: 0.7.11
138
+ version: '0.7'
139
139
  - !ruby/object:Gem::Dependency
140
140
  name: rake
141
141
  requirement: !ruby/object:Gem::Requirement
@@ -156,14 +156,14 @@ dependencies:
156
156
  requirements:
157
157
  - - "~>"
158
158
  - !ruby/object:Gem::Version
159
- version: 0.29.1
159
+ version: '0.29'
160
160
  type: :development
161
161
  prerelease: false
162
162
  version_requirements: !ruby/object:Gem::Requirement
163
163
  requirements:
164
164
  - - "~>"
165
165
  - !ruby/object:Gem::Version
166
- version: 0.29.1
166
+ version: '0.29'
167
167
  - !ruby/object:Gem::Dependency
168
168
  name: rspec
169
169
  requirement: !ruby/object:Gem::Requirement