right_api_client 1.5.22 → 1.5.23

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.
@@ -1,7 +1,10 @@
1
1
  # CHANGELOG.md
2
2
 
3
3
  ## Next
4
- - <Your change here>
4
+
5
+ ## 1.5.23
6
+ - \#78 Prevent logging of credentials during login requests
7
+ - \#77 Add support for OAuth2 authentication via refresh token
5
8
 
6
9
  ## 1.5.22
7
10
  - \#76 Add ability to directly access attributes of a ResourceDetail
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.5.22
1
+ 1.5.23
@@ -12,6 +12,43 @@ require File.expand_path('../resources', __FILE__)
12
12
  require File.expand_path('../errors', __FILE__)
13
13
  require File.expand_path('../exceptions', __FILE__)
14
14
 
15
+ # This is used to extend a temporary local copy of the client during login.
16
+ # It overrides the post functionality, which in turn creates a new instance of RestClient::Request
17
+ # which is then extended to override log_request to keep creds from getting into our logs.
18
+ module PostOverride
19
+ def post(payload, additional_headers={}, &block)
20
+ headers = (options[:headers] || {}).merge(additional_headers)
21
+ requestor = ::RestClient::Request.new(options.merge(
22
+ :method => :post,
23
+ :url => url,
24
+ :payload => payload,
25
+ :headers => headers)
26
+ )
27
+ requestor.extend(LogOverride)
28
+ requestor.execute(&block)
29
+ end
30
+ end
31
+
32
+ # This is used to extend a new instance of RestClient::Request and override it's log_request.
33
+ # Override version keeps email/password from getting into the logs.
34
+ module LogOverride
35
+ def log_request
36
+ if RestClient.log
37
+ out = []
38
+ out << "RestClient.#{method} #{url.inspect}"
39
+ if !payload.nil?
40
+ if (payload.short_inspect.include? "email") || (payload.short_inspect.include? "password")
41
+ out << "<hidden credentials>"
42
+ else
43
+ out << payload.short_inspect
44
+ end
45
+ end
46
+ out << processed_headers.to_a.sort.map { |(k, v)| [k.inspect, v.inspect].join("=>") }.join(", ")
47
+ RestClient.log << out.join(', ') + "\n"
48
+ end
49
+ end
50
+ end
51
+
15
52
  # RightApiClient has the generic get/post/delete/put calls that are used by resources
16
53
  module RightApi
17
54
  class Client
@@ -21,22 +58,60 @@ module RightApi
21
58
  DEFAULT_TIMEOUT = -1
22
59
  DEFAULT_MAX_ATTEMPTS = 5
23
60
 
24
- ROOT_RESOURCE = '/api/session'
61
+ ROOT_RESOURCE = '/api/session'
62
+ OAUTH_ENDPOINT = '/api/oauth2'
25
63
  ROOT_INSTANCE_RESOURCE = '/api/session/instance'
26
64
  DEFAULT_API_URL = 'https://my.rightscale.com'
27
65
 
28
66
  # permitted parameters for initializing
29
67
  AUTH_PARAMS = %w[
30
- email password_base64 password account_id api_url api_version
31
- cookies instance_token access_token timeout open_timeout max_attempts
68
+ email password_base64 password
69
+ instance_token
70
+ refresh_token access_token
71
+ cookies
72
+ account_id api_url api_version
73
+ timeout open_timeout max_attempts
32
74
  enable_retry rest_client_class
33
75
  ]
34
76
 
35
- attr_reader :cookies, :instance_token, :access_token, :last_request, :timeout, :open_timeout, :max_attempts, :enable_retry
36
- attr_accessor :account_id, :api_url
77
+ # @return [String] OAuth 2.0 refresh token if provided
78
+ attr_reader :refresh_token
37
79
 
38
- def initialize(args)
80
+ # @return [String] OAuth 2.0 access token, if present
81
+ attr_reader :access_token
82
+
83
+ # @return [Time] expiry timestamp for OAuth 2.0 access token
84
+ attr_reader :access_token_expires_at
85
+
86
+ attr_accessor :account_id
87
+
88
+ # @return [String] Base API url, e.g. https://us-3.rightscale.com
89
+ attr_accessor :api_url
90
+
91
+ # @return [String] instance API token as included in user-data
92
+ attr_reader :instance_token
93
+
94
+ # @return [Hash] collection of API cookies
95
+ # @deprecated please use OAuth 2.0 refresh tokens instead of password-based authentication
96
+ attr_reader :cookies
97
+
98
+ # @return [Hash] debug information about the last request and response
99
+ attr_reader :last_request
100
+
101
+ # @return [Integer] number of seconds to wait for socket open
102
+ attr_reader :open_timeout
103
+
104
+ # @return [Integer] number of seconds to wait for API response
105
+ attr_reader :timeout
106
+
107
+ # @return [Integer] number of times to retry idempotent requests (iff enable_retry == true)
108
+ attr_reader :max_attempts
109
+
110
+ # @return [Boolean] whether to retry idempotent requests that fail
111
+ attr_reader :enable_retry
39
112
 
113
+ # Instantiate a new Client.
114
+ def initialize(args)
40
115
  raise 'This API client is only compatible with Ruby 1.8.7 and upwards.' if (RUBY_VERSION < '1.8.7')
41
116
 
42
117
  @api_url, @api_version = DEFAULT_API_URL, API_VERSION
@@ -55,8 +130,9 @@ module RightApi
55
130
  @rest_client = @rest_client_class.new(@api_url, :open_timeout => @open_timeout, :timeout => @timeout)
56
131
  @last_request = {}
57
132
 
58
- # There are four options for login:
59
- # - credentials
133
+ # There are five options for login:
134
+ # - user email/password (using plaintext or base64-obfuscated password)
135
+ # - user OAuth refresh token
60
136
  # - instance API token
61
137
  # - existing user-supplied cookies
62
138
  # - existing user-supplied OAuth access token
@@ -103,7 +179,7 @@ module RightApi
103
179
  end
104
180
 
105
181
  def to_s
106
- "#<RightApi::Client>"
182
+ "#<RightApi::Client #{api_url}>"
107
183
  end
108
184
 
109
185
  # Log HTTP calls to file (file can be STDOUT as well)
@@ -127,7 +203,6 @@ module RightApi
127
203
  # so this is a workaround.
128
204
  #
129
205
  def resources(type, path)
130
-
131
206
  Resources.new(self, path, type)
132
207
  end
133
208
 
@@ -162,7 +237,7 @@ module RightApi
162
237
  end
163
238
  rescue ApiError => e
164
239
  if re_login?(e)
165
- # Session cookie is expired or invalid
240
+ # Session is expired or invalid
166
241
  login()
167
242
  retry
168
243
  else
@@ -172,25 +247,36 @@ module RightApi
172
247
  end
173
248
 
174
249
  def login
175
- params, path = if @instance_token
176
- [ { 'instance_token' => @instance_token },
177
- ROOT_INSTANCE_RESOURCE ]
250
+ account_href = "/api/accounts/#{@account_id}"
251
+
252
+ params, path =
253
+ if @refresh_token
254
+ [ {'grant_type' => 'refresh_token',
255
+ 'refresh_token'=>@refresh_token},
256
+ OAUTH_ENDPOINT ]
257
+ elsif @instance_token
258
+ [ { 'instance_token' => @instance_token,
259
+ 'account_href' => account_href },
260
+ ROOT_INSTANCE_RESOURCE ]
178
261
  elsif @password_base64
179
- [ { 'email' => @email, 'password' => Base64.decode64(@password_base64) },
262
+ [ { 'email' => @email,
263
+ 'password' => Base64.decode64(@password_base64),
264
+ 'account_href' => account_href },
180
265
  ROOT_RESOURCE ]
181
266
  else
182
- [ { 'email' => @email, 'password' => @password },
267
+ [ { 'email' => @email,
268
+ 'password' => @password,
269
+ 'account_href' => account_href },
183
270
  ROOT_RESOURCE ]
184
271
  end
185
- params['account_href'] = "/api/accounts/#{@account_id}"
186
272
 
187
273
  response = nil
188
274
  attempts = 0
189
275
  begin
190
- response = @rest_client[path].post(params, 'X-Api-Version' => @api_version) do |response, request, result, &block|
191
- if response.code == 302
276
+ response = @rest_client[path].extend(PostOverride).post(params, 'X-Api-Version' => @api_version) do |response, request, result, &block|
277
+ if [301, 302, 307].include?(response.code)
192
278
  update_api_url(response)
193
- response.follow_redirection(request, result, &block)
279
+ response = @rest_client[path].extend(PostOverride).post(params, 'X-Api-Version' => @api_version)
194
280
  else
195
281
  response.return!(request, result)
196
282
  end
@@ -202,17 +288,24 @@ module RightApi
202
288
  retry
203
289
  end
204
290
 
205
- update_cookies(response)
291
+ if path == OAUTH_ENDPOINT
292
+ update_access_token(response)
293
+ else
294
+ update_cookies(response)
295
+ end
206
296
  end
207
297
 
208
298
  # Returns the request headers
209
299
  def headers
210
300
  h = {
211
301
  'X-Api-Version' => @api_version,
212
- 'X-Account' => @account_id,
213
302
  :accept => :json,
214
303
  }
215
304
 
305
+ if @account_id
306
+ h['X-Account'] = @account_id
307
+ end
308
+
216
309
  if @access_token
217
310
  h['Authorization'] = "Bearer #{@access_token}"
218
311
  elsif @cookies
@@ -230,6 +323,7 @@ module RightApi
230
323
  # Generic get
231
324
  # params are NOT read only
232
325
  def do_get(path, params={})
326
+ login if need_login?
233
327
 
234
328
  # Resource id is a special param as it needs to be added to the path
235
329
  path = add_id_and_params_to_path(path, params)
@@ -280,6 +374,8 @@ module RightApi
280
374
 
281
375
  # Generic post
282
376
  def do_post(path, params={})
377
+ login if need_login?
378
+
283
379
  params = fix_array_of_hashes(params)
284
380
 
285
381
  req, res, resource_type, body = nil
@@ -334,6 +430,8 @@ module RightApi
334
430
 
335
431
  # Generic delete
336
432
  def do_delete(path, params={})
433
+ login if need_login?
434
+
337
435
  # Resource id is a special param as it needs to be added to the path
338
436
  path = add_id_and_params_to_path(path, params)
339
437
 
@@ -367,6 +465,8 @@ module RightApi
367
465
 
368
466
  # Generic put
369
467
  def do_put(path, params={})
468
+ login if need_login?
469
+
370
470
  params = fix_array_of_hashes(params)
371
471
 
372
472
  req, res, resource_type, body = nil
@@ -396,14 +496,31 @@ module RightApi
396
496
  end
397
497
  end
398
498
 
499
+ # Determine whether the client needs a fresh round of authentication based on state of
500
+ # cookies/tokens and their expiration timestamps.
501
+ #
502
+ # @return [Boolean] true if re-login is suggested
503
+ def need_login?
504
+ (@refresh_token && @refresh_token_expires_at && @refresh_token_expires_at - Time.now < 900) ||
505
+ (@cookies.respond_to?(:empty?) && @cookies.empty?)
506
+ end
507
+
508
+ # Determine whether an exception can be fixed by logging in again.
509
+ #
510
+ # @return [Boolean] true if re-login is appropriate
399
511
  def re_login?(e)
400
- # cannot successfully re-login with only an access token; we want the
401
- # expiration error to be raised.
402
- return false if @access_token
403
- e.message.index('403') && e.message =~ %r(.*Session cookie is expired or invalid)
512
+ auth_error =
513
+ (e.message.index('403') && e.message =~ %r(.*Session cookie is expired or invalid)) ||
514
+ e.message.index('401')
515
+
516
+ renewable_creds =
517
+ (@instance_token || (@email && (@password || @password_base64)) || @refresh_token)
518
+
519
+ auth_error && renewable_creds
404
520
  end
405
521
 
406
- # returns the resource_type
522
+ # @param [String] content_type an HTTP Content-Type header
523
+ # @return [String] the resource_type associated with content_type
407
524
  def get_resource_type(content_type)
408
525
  content_type.scan(/\.rightscale\.(.*)\+json/)[0][0]
409
526
  end
@@ -411,17 +528,23 @@ module RightApi
411
528
  # Makes sure the @cookies have a timestamp.
412
529
  #
413
530
  def timestamp_cookies
414
-
415
531
  return unless @cookies
416
532
 
417
533
  class << @cookies; attr_accessor :timestamp; end
418
534
  @cookies.timestamp = Time.now
419
535
  end
420
536
 
537
+ # Sets the @access_token and @access_token_expires_at
538
+ #
539
+ def update_access_token(response)
540
+ h = JSON.load(response)
541
+ @access_token = String(h['access_token'])
542
+ @access_token_expires_at = Time.at(Time.now.to_i + Integer(h['expires_in']))
543
+ end
544
+
421
545
  # Sets the @cookies (and timestamp it).
422
546
  #
423
547
  def update_cookies(response)
424
-
425
548
  return unless response.cookies
426
549
 
427
550
  (@cookies ||= {}).merge!(response.cookies)
@@ -432,7 +555,6 @@ module RightApi
432
555
  # A helper class for error details
433
556
  #
434
557
  class ErrorDetails
435
-
436
558
  attr_reader :method, :path, :params, :request, :response
437
559
 
438
560
  def initialize(me, pt, ps, rq, rs)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: right_api_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.22
4
+ version: 1.5.23
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-09-19 00:00:00.000000000 Z
12
+ date: 2014-10-15 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: json
@@ -174,7 +174,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
174
174
  version: '0'
175
175
  segments:
176
176
  - 0
177
- hash: -3655721947136781612
177
+ hash: 681643300708536250
178
178
  required_rubygems_version: !ruby/object:Gem::Requirement
179
179
  none: false
180
180
  requirements: