right_api_client 1.5.22 → 1.5.23
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +4 -1
- data/VERSION +1 -1
- data/lib/right_api_client/client.rb +152 -30
- metadata +3 -3
data/CHANGELOG.md
CHANGED
@@ -1,7 +1,10 @@
|
|
1
1
|
# CHANGELOG.md
|
2
2
|
|
3
3
|
## Next
|
4
|
-
|
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.
|
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
|
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
|
31
|
-
|
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
|
-
|
36
|
-
|
77
|
+
# @return [String] OAuth 2.0 refresh token if provided
|
78
|
+
attr_reader :refresh_token
|
37
79
|
|
38
|
-
|
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
|
59
|
-
# -
|
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
|
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
|
-
|
176
|
-
|
177
|
-
|
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,
|
262
|
+
[ { 'email' => @email,
|
263
|
+
'password' => Base64.decode64(@password_base64),
|
264
|
+
'account_href' => account_href },
|
180
265
|
ROOT_RESOURCE ]
|
181
266
|
else
|
182
|
-
[ { 'email' => @email,
|
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
|
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.
|
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
|
-
|
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
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
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
|
-
#
|
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.
|
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-
|
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:
|
177
|
+
hash: 681643300708536250
|
178
178
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
179
179
|
none: false
|
180
180
|
requirements:
|