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.
- 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:
|