ibm_cloud_sdk_core 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +3 -0
- data/lib/ibm_cloud_sdk_core.rb +6 -0
- data/lib/ibm_cloud_sdk_core/api_exception.rb +41 -0
- data/lib/ibm_cloud_sdk_core/base_service.rb +284 -0
- data/lib/ibm_cloud_sdk_core/detailed_response.rb +23 -0
- data/lib/ibm_cloud_sdk_core/iam_token_manager.rb +150 -0
- data/lib/ibm_cloud_sdk_core/version.rb +5 -0
- data/rakefile +44 -0
- data/test/test_helper.rb +28 -0
- data/test/unit/test_base_service.rb +304 -0
- data/test/unit/test_configure_http_proxy.rb +152 -0
- data/test/unit/test_detailed_response.rb +26 -0
- data/test/unit/test_iam_token_manager.rb +228 -0
- metadata +254 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a4246cbde9bd7f851cb748bec1b534632334e64508202a822810d0e2bb2f5de6
|
4
|
+
data.tar.gz: 5f388df8543fd24288f66edb14ce0e11394c3d76aa89de1830442d3b51b78fd2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 57496087e3feb73ff6f6e8714ee22405cd43aaace2493af2c3b8d824b3ee198b3579a43c48f37b771bcbb9cca80b49c489c8659c9eba842673043b60b8c61287
|
7
|
+
data.tar.gz: 512e10063200783cf8032a1b1545df28244b7aad50bd1d5ba1653c4f010b082c2f3f03080b0a1c41490410da5687879b9a16c9ad5e7ac8404f549a308f069ee0
|
data/README.md
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require("json")
|
4
|
+
|
5
|
+
module IBMCloudSdkCore
|
6
|
+
# Custom exception class for errors returned from the APIs
|
7
|
+
class ApiException < StandardError
|
8
|
+
attr_reader :code, :error, :info, :transaction_id, :global_transaction_id
|
9
|
+
# :param HTTP::Response response: The response object from the API
|
10
|
+
def initialize(code: nil, error: nil, info: nil, transaction_id: nil, global_transaction_id: nil, response: nil)
|
11
|
+
if code.nil? || error.nil?
|
12
|
+
@code = response.code
|
13
|
+
@error = response.reason
|
14
|
+
unless response.body.empty?
|
15
|
+
body_hash = JSON.parse(response.body.to_s)
|
16
|
+
error_message = body_hash["errors"] && body_hash["errors"][0] ? body_hash["errors"][0].message : nil
|
17
|
+
@code = body_hash["code"] || body_hash["error_code"] || body_hash["status"]
|
18
|
+
@error = error_message || body_hash["error"] || body_hash["message"]
|
19
|
+
%w[code error_code status errors error message].each { |k| body_hash.delete(k) }
|
20
|
+
@info = body_hash
|
21
|
+
end
|
22
|
+
else
|
23
|
+
# :nocov:
|
24
|
+
@code = code
|
25
|
+
@error = error
|
26
|
+
@info = info
|
27
|
+
# :nocov:
|
28
|
+
end
|
29
|
+
@transaction_id = transaction_id
|
30
|
+
@global_transaction_id = global_transaction_id
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_s
|
34
|
+
msg = "Error: #{@error}, Code: #{@code}"
|
35
|
+
msg += ", Information: #{@info}" unless @info.nil?
|
36
|
+
msg += ", X-dp-watson-tran-id: #{@transaction_id}" unless @transaction_id.nil?
|
37
|
+
msg += ", X-global-transaction-id: #{@global_transaction_id}" unless @global_transaction_id.nil?
|
38
|
+
msg
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,284 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require("http")
|
4
|
+
require("rbconfig")
|
5
|
+
require("stringio")
|
6
|
+
require("json")
|
7
|
+
require_relative("./detailed_response.rb")
|
8
|
+
require_relative("./api_exception.rb")
|
9
|
+
require_relative("./iam_token_manager.rb")
|
10
|
+
require_relative("./version.rb")
|
11
|
+
|
12
|
+
DEFAULT_CREDENTIALS_FILE_NAME = "ibm-credentials.env"
|
13
|
+
NORMALIZER = lambda do |uri| # Custom URI normalizer when using HTTP Client
|
14
|
+
HTTP::URI.parse uri
|
15
|
+
end
|
16
|
+
|
17
|
+
module IBMCloudSdkCore
|
18
|
+
# Class for interacting with the API
|
19
|
+
class BaseService
|
20
|
+
attr_accessor :password, :url, :username, :display_name
|
21
|
+
attr_reader :conn, :token_manager
|
22
|
+
def initialize(vars)
|
23
|
+
defaults = {
|
24
|
+
vcap_services_name: nil,
|
25
|
+
username: nil,
|
26
|
+
password: nil,
|
27
|
+
use_vcap_services: true,
|
28
|
+
iam_apikey: nil,
|
29
|
+
iam_access_token: nil,
|
30
|
+
iam_url: nil,
|
31
|
+
display_name: nil
|
32
|
+
}
|
33
|
+
vars = defaults.merge(vars)
|
34
|
+
@url = vars[:url]
|
35
|
+
@username = nil
|
36
|
+
@password = nil
|
37
|
+
@iam_apikey = nil
|
38
|
+
@token_manager = nil
|
39
|
+
@temp_headers = nil
|
40
|
+
@icp_prefix = vars[:password]&.start_with?("icp-") || vars[:iam_apikey]&.start_with?("icp-") ? true : false
|
41
|
+
@disable_ssl_verification = false
|
42
|
+
@display_name = vars[:display_name]
|
43
|
+
|
44
|
+
if (!vars[:iam_access_token].nil? || !vars[:iam_apikey].nil?) && !@icp_prefix
|
45
|
+
set_token_manager(iam_apikey: vars[:iam_apikey], iam_access_token: vars[:iam_access_token], iam_url: vars[:iam_url])
|
46
|
+
elsif !vars[:iam_apikey].nil? && @icp_prefix
|
47
|
+
@username = "apikey"
|
48
|
+
@password = vars[:iam_apikey]
|
49
|
+
elsif !vars[:username].nil? && !vars[:password].nil?
|
50
|
+
if vars[:username] == "apikey" && !@icp_prefix
|
51
|
+
iam_apikey(iam_apikey: vars[:password])
|
52
|
+
else
|
53
|
+
@username = vars[:username]
|
54
|
+
@password = vars[:password]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
if @display_name && !@username && !@iam_apikey
|
59
|
+
service_name = @display_name.sub(" ", "_").downcase
|
60
|
+
load_from_credential_file(service_name)
|
61
|
+
@icp_prefix = @password&.start_with?("icp-") || @iam_apikey&.start_with?("icp-") ? true : false
|
62
|
+
end
|
63
|
+
|
64
|
+
if vars[:use_vcap_services] && !@username && !@iam_apikey
|
65
|
+
@vcap_service_credentials = load_from_vcap_services(service_name: vars[:vcap_services_name])
|
66
|
+
if !@vcap_service_credentials.nil? && @vcap_service_credentials.instance_of?(Hash)
|
67
|
+
@url = @vcap_service_credentials["url"]
|
68
|
+
@username = @vcap_service_credentials["username"] if @vcap_service_credentials.key?("username")
|
69
|
+
@password = @vcap_service_credentials["password"] if @vcap_service_credentials.key?("password")
|
70
|
+
@iam_apikey = @vcap_service_credentials["iam_apikey"] if @vcap_service_credentials.key?("iam_apikey")
|
71
|
+
@iam_access_token = @vcap_service_credentials["iam_access_token"] if @vcap_service_credentials.key?("iam_access_token")
|
72
|
+
@iam_url = @vcap_service_credentials["iam_url"] if @vcap_service_credentials.key?("iam_url")
|
73
|
+
@icp_prefix = @password&.start_with?("icp-") || @iam_apikey&.start_with?("icp-") ? true : false
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
raise ArgumentError.new('The username shouldn\'t start or end with curly brackets or quotes. Be sure to remove any {} and \" characters surrounding your username') if check_bad_first_or_last_char(@username)
|
78
|
+
raise ArgumentError.new('The password shouldn\'t start or end with curly brackets or quotes. Be sure to remove any {} and \" characters surrounding your password') if check_bad_first_or_last_char(@password)
|
79
|
+
raise ArgumentError.new('The url shouldn\'t start or end with curly brackets or quotes. Be sure to remove any {} and \" characters surrounding your url') if check_bad_first_or_last_char(@url)
|
80
|
+
raise ArgumentError.new('The apikey shouldn\'t start or end with curly brackets or quotes. Be sure to remove any {} and \" characters surrounding your apikey') if check_bad_first_or_last_char(@iam_apikey)
|
81
|
+
|
82
|
+
@conn = HTTP::Client.new(
|
83
|
+
headers: {}
|
84
|
+
).use normalize_uri: { normalizer: NORMALIZER }
|
85
|
+
end
|
86
|
+
|
87
|
+
# Initiates the credentials based on the credential file
|
88
|
+
def load_from_credential_file(service_name, separator = "=")
|
89
|
+
credential_file_path = ENV["IBM_CREDENTIALS_FILE"]
|
90
|
+
|
91
|
+
# Home directory
|
92
|
+
if credential_file_path.nil?
|
93
|
+
file_path = ENV["HOME"] + DEFAULT_CREDENTIALS_FILE_NAME
|
94
|
+
credential_file_path = file_path if File.exist?(file_path)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Top-level directory of the project
|
98
|
+
if credential_file_path.nil?
|
99
|
+
file_path = File.join(File.dirname(__FILE__), "/../../" + DEFAULT_CREDENTIALS_FILE_NAME)
|
100
|
+
credential_file_path = file_path if File.exist?(file_path)
|
101
|
+
end
|
102
|
+
|
103
|
+
return if credential_file_path.nil?
|
104
|
+
|
105
|
+
file_contents = File.open(credential_file_path, "r")
|
106
|
+
file_contents.each_line do |line|
|
107
|
+
key_val = line.strip.split(separator)
|
108
|
+
set_credential_based_on_type(service_name, key_val[0].downcase, key_val[1]) if key_val.length == 2
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def load_from_vcap_services(service_name:)
|
113
|
+
vcap_services = ENV["VCAP_SERVICES"]
|
114
|
+
unless vcap_services.nil?
|
115
|
+
services = JSON.parse(vcap_services)
|
116
|
+
return services[service_name][0]["credentials"] if services.key?(service_name)
|
117
|
+
end
|
118
|
+
nil
|
119
|
+
end
|
120
|
+
|
121
|
+
def add_default_headers(headers: {})
|
122
|
+
raise TypeError unless headers.instance_of?(Hash)
|
123
|
+
|
124
|
+
headers.each_pair { |k, v| @conn.default_options.headers.add(k, v) }
|
125
|
+
end
|
126
|
+
|
127
|
+
def iam_access_token(iam_access_token:)
|
128
|
+
@token_manager = IAMTokenManager.new(iam_access_token: iam_access_token) if @token_manager.nil?
|
129
|
+
@iam_access_token = iam_access_token
|
130
|
+
end
|
131
|
+
|
132
|
+
def iam_apikey(iam_apikey:)
|
133
|
+
@token_manager = IAMTokenManager.new(iam_apikey: iam_apikey) if @token_manager.nil?
|
134
|
+
@iam_apikey = iam_apikey
|
135
|
+
end
|
136
|
+
|
137
|
+
# @return [DetailedResponse]
|
138
|
+
def request(args)
|
139
|
+
defaults = { method: nil, url: nil, accept_json: false, headers: nil, params: nil, json: {}, data: nil }
|
140
|
+
args = defaults.merge(args)
|
141
|
+
args[:data].delete_if { |_k, v| v.nil? } if args[:data].instance_of?(Hash)
|
142
|
+
args[:json] = args[:data].merge(args[:json]) if args[:data].respond_to?(:merge)
|
143
|
+
args[:json] = args[:data] if args[:json].empty? || (args[:data].instance_of?(String) && !args[:data].empty?)
|
144
|
+
args[:json].delete_if { |_k, v| v.nil? } if args[:json].instance_of?(Hash)
|
145
|
+
args[:headers]["Accept"] = "application/json" if args[:accept_json] && args[:headers]["Accept"].nil?
|
146
|
+
args[:headers]["Content-Type"] = "application/json" unless args[:headers].key?("Content-Type")
|
147
|
+
args[:json] = args[:json].to_json if args[:json].instance_of?(Hash)
|
148
|
+
args[:headers].delete_if { |_k, v| v.nil? } if args[:headers].instance_of?(Hash)
|
149
|
+
args[:params].delete_if { |_k, v| v.nil? } if args[:params].instance_of?(Hash)
|
150
|
+
args[:form].delete_if { |_k, v| v.nil? } if args.key?(:form)
|
151
|
+
args.delete_if { |_, v| v.nil? }
|
152
|
+
args[:headers].delete("Content-Type") if args.key?(:form) || args[:json].nil?
|
153
|
+
|
154
|
+
if @username == "apikey" && !@icp_prefix
|
155
|
+
iam_apikey(iam_apikey: @password)
|
156
|
+
@username = nil
|
157
|
+
end
|
158
|
+
|
159
|
+
conn = @conn
|
160
|
+
if !@iam_apikey.nil? && @icp_prefix
|
161
|
+
conn = @conn.basic_auth(user: "apikey", pass: @iam_apikey)
|
162
|
+
elsif !@token_manager.nil?
|
163
|
+
access_token = @token_manager.token
|
164
|
+
args[:headers]["Authorization"] = "Bearer #{access_token}"
|
165
|
+
elsif !@username.nil? && !@password.nil?
|
166
|
+
conn = @conn.basic_auth(user: @username, pass: @password)
|
167
|
+
end
|
168
|
+
|
169
|
+
args[:headers] = args[:headers].merge(@temp_headers) unless @temp_headers.nil?
|
170
|
+
@temp_headers = nil unless @temp_headers.nil?
|
171
|
+
|
172
|
+
if args.key?(:form)
|
173
|
+
response = conn.follow.request(
|
174
|
+
args[:method],
|
175
|
+
HTTP::URI.parse(@url + args[:url]),
|
176
|
+
headers: conn.default_options.headers.merge(HTTP::Headers.coerce(args[:headers])),
|
177
|
+
params: args[:params],
|
178
|
+
form: args[:form]
|
179
|
+
)
|
180
|
+
else
|
181
|
+
response = conn.follow.request(
|
182
|
+
args[:method],
|
183
|
+
HTTP::URI.parse(@url + args[:url]),
|
184
|
+
headers: conn.default_options.headers.merge(HTTP::Headers.coerce(args[:headers])),
|
185
|
+
body: args[:json],
|
186
|
+
params: args[:params]
|
187
|
+
)
|
188
|
+
end
|
189
|
+
return DetailedResponse.new(response: response) if (200..299).cover?(response.code)
|
190
|
+
|
191
|
+
raise ApiException.new(response: response)
|
192
|
+
end
|
193
|
+
|
194
|
+
# @note Chainable
|
195
|
+
# @param headers [Hash] Custom headers to be sent with the request
|
196
|
+
# @return [self]
|
197
|
+
def headers(headers)
|
198
|
+
raise TypeError("Expected Hash type, received #{headers.class}") unless headers.instance_of?(Hash)
|
199
|
+
|
200
|
+
@temp_headers = headers
|
201
|
+
self
|
202
|
+
end
|
203
|
+
|
204
|
+
# @!method configure_http_client(proxy: {}, timeout: {}, disable_ssl_verification: false)
|
205
|
+
# Sets the http client config, currently works with timeout and proxies
|
206
|
+
# @param proxy [Hash] The hash of proxy configurations
|
207
|
+
# @option proxy address [String] The address of the proxy
|
208
|
+
# @option proxy port [Integer] The port of the proxy
|
209
|
+
# @option proxy username [String] The username of the proxy, if authentication is needed
|
210
|
+
# @option proxy password [String] The password of the proxy, if authentication is needed
|
211
|
+
# @option proxy headers [Hash] The headers to be used with the proxy
|
212
|
+
# @param timeout [Hash] The hash for configuring timeouts. `per_operation` has priority over `global`
|
213
|
+
# @option timeout per_operation [Hash] Timeouts per operation. Requires `read`, `write`, `connect`
|
214
|
+
# @option timeout global [Integer] Upper bound on total request time
|
215
|
+
# @param disable_ssl_verification [Boolean] Disable the SSL verification (Note that this has serious security implications - only do this if you really mean to!)
|
216
|
+
def configure_http_client(proxy: {}, timeout: {}, disable_ssl_verification: false)
|
217
|
+
raise TypeError("proxy parameter must be a Hash") unless proxy.empty? || proxy.instance_of?(Hash)
|
218
|
+
|
219
|
+
raise TypeError("timeout parameter must be a Hash") unless timeout.empty? || timeout.instance_of?(Hash)
|
220
|
+
|
221
|
+
@disable_ssl_verification = disable_ssl_verification
|
222
|
+
if disable_ssl_verification
|
223
|
+
ssl_context = OpenSSL::SSL::SSLContext.new
|
224
|
+
ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
225
|
+
@conn.default_options = { ssl_context: ssl_context }
|
226
|
+
end
|
227
|
+
add_proxy(proxy) unless proxy.empty? || !proxy.dig(:address).is_a?(String) || !proxy.dig(:port).is_a?(Integer)
|
228
|
+
add_timeout(timeout) unless timeout.empty? || (!timeout.key?(:per_operation) && !timeout.key?(:global))
|
229
|
+
end
|
230
|
+
|
231
|
+
private
|
232
|
+
|
233
|
+
def set_credential_based_on_type(service_name, key, value)
|
234
|
+
return unless key.include?(service_name)
|
235
|
+
|
236
|
+
@iam_apikey = value if key.include?("iam_apikey")
|
237
|
+
@iam_url = value if key.include?("iam_url")
|
238
|
+
@url = value if key.include?("url")
|
239
|
+
@username = value if key.include?("username")
|
240
|
+
@password = value if key.include?("password")
|
241
|
+
end
|
242
|
+
|
243
|
+
def check_bad_first_or_last_char(str)
|
244
|
+
return str.start_with?("{", "\"") || str.end_with?("}", "\"") unless str.nil?
|
245
|
+
end
|
246
|
+
|
247
|
+
def set_token_manager(iam_apikey: nil, iam_access_token: nil, iam_url: nil)
|
248
|
+
@iam_apikey = iam_apikey
|
249
|
+
@iam_access_token = iam_access_token
|
250
|
+
@iam_url = iam_url
|
251
|
+
@token_manager = IAMTokenManager.new(iam_apikey: iam_apikey, iam_access_token: iam_access_token, iam_url: iam_url)
|
252
|
+
end
|
253
|
+
|
254
|
+
def add_timeout(timeout)
|
255
|
+
if timeout.key?(:per_operation)
|
256
|
+
raise TypeError("per_operation in timeout must be a Hash") unless timeout[:per_operation].instance_of?(Hash)
|
257
|
+
|
258
|
+
defaults = {
|
259
|
+
write: 0,
|
260
|
+
connect: 0,
|
261
|
+
read: 0
|
262
|
+
}
|
263
|
+
time = defaults.merge(timeout[:per_operation])
|
264
|
+
@conn = @conn.timeout(write: time[:write], connect: time[:connect], read: time[:read])
|
265
|
+
else
|
266
|
+
raise TypeError("global in timeout must be an Integer") unless timeout[:global].is_a?(Integer)
|
267
|
+
|
268
|
+
@conn = @conn.timeout(timeout[:global])
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
def add_proxy(proxy)
|
273
|
+
if (proxy[:username].nil? || proxy[:password].nil?) && proxy[:headers].nil?
|
274
|
+
@conn = @conn.via(proxy[:address], proxy[:port])
|
275
|
+
elsif !proxy[:username].nil? && !proxy[:password].nil? && proxy[:headers].nil?
|
276
|
+
@conn = @conn.via(proxy[:address], proxy[:port], proxy[:username], proxy[:password])
|
277
|
+
elsif !proxy[:headers].nil? && (proxy[:username].nil? || proxy[:password].nil?)
|
278
|
+
@conn = @conn.via(proxy[:address], proxy[:port], proxy[:headers])
|
279
|
+
else
|
280
|
+
@conn = @conn.via(proxy[:address], proxy[:port], proxy[:username], proxy[:password], proxy[:headers])
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require("json")
|
4
|
+
|
5
|
+
module IBMCloudSdkCore
|
6
|
+
# Custom class for objects returned from API calls
|
7
|
+
class DetailedResponse
|
8
|
+
attr_reader :status, :headers, :result
|
9
|
+
def initialize(status: nil, headers: nil, body: nil, response: nil)
|
10
|
+
if status.nil? || headers.nil? || body.nil?
|
11
|
+
@status = response.code
|
12
|
+
@headers = response.headers.to_h
|
13
|
+
@headers = response.headers.to_hash if response.headers.respond_to?("to_hash")
|
14
|
+
@result = response.body.to_s
|
15
|
+
@result = JSON.parse(response.body.to_s) if !response.body.to_s.empty? && @headers.key?("Content-Type") && @headers["Content-Type"].start_with?("application/json")
|
16
|
+
else
|
17
|
+
@status = status
|
18
|
+
@headers = headers
|
19
|
+
@result = body
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require("http")
|
4
|
+
require("json")
|
5
|
+
require("rbconfig")
|
6
|
+
require_relative("./version.rb")
|
7
|
+
|
8
|
+
module IBMCloudSdkCore
|
9
|
+
# Class to manage IAM Token Authentication
|
10
|
+
class IAMTokenManager
|
11
|
+
DEFAULT_IAM_URL = "https://iam.bluemix.net/identity/token"
|
12
|
+
CONTENT_TYPE = "application/x-www-form-urlencoded"
|
13
|
+
ACCEPT = "application/json"
|
14
|
+
DEFAULT_AUTHORIZATION = "Basic Yng6Yng="
|
15
|
+
REQUEST_TOKEN_GRANT_TYPE = "urn:ibm:params:oauth:grant-type:apikey"
|
16
|
+
REQUEST_TOKEN_RESPONSE_TYPE = "cloud_iam"
|
17
|
+
REFRESH_TOKEN_GRANT_TYPE = "refresh_token"
|
18
|
+
|
19
|
+
attr_accessor :token_info, :user_access_token
|
20
|
+
def initialize(iam_apikey: nil, iam_access_token: nil, iam_url: nil)
|
21
|
+
@iam_apikey = iam_apikey
|
22
|
+
@user_access_token = iam_access_token
|
23
|
+
@iam_url = iam_url.nil? ? DEFAULT_IAM_URL : iam_url
|
24
|
+
@token_info = {
|
25
|
+
"access_token" => nil,
|
26
|
+
"refresh_token" => nil,
|
27
|
+
"token_type" => nil,
|
28
|
+
"expires_in" => nil,
|
29
|
+
"expiration" => nil
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def request(method:, url:, headers: nil, params: nil, data: nil)
|
34
|
+
response = nil
|
35
|
+
if headers.key?("Content-Type") && headers["Content-Type"] == CONTENT_TYPE
|
36
|
+
response = HTTP.request(
|
37
|
+
method,
|
38
|
+
url,
|
39
|
+
body: HTTP::URI.form_encode(data),
|
40
|
+
headers: headers,
|
41
|
+
params: params
|
42
|
+
)
|
43
|
+
end
|
44
|
+
return JSON.parse(response.body.to_s) if (200..299).cover?(response.code)
|
45
|
+
|
46
|
+
require_relative("./api_exception.rb")
|
47
|
+
raise ApiException.new(response: response)
|
48
|
+
end
|
49
|
+
|
50
|
+
# The source of the token is determined by the following logic:
|
51
|
+
# 1. If user provides their own managed access token, assume it is valid and send it
|
52
|
+
# 2. If this class is managing tokens and does not yet have one, make a request for one
|
53
|
+
# 3. If this class is managing tokens and the token has expired refresh it. In case the refresh token is expired, get a new one
|
54
|
+
# If this class is managing tokens and has a valid token stored, send it
|
55
|
+
def token
|
56
|
+
return @user_access_token unless @user_access_token.nil? || (@user_access_token.respond_to?(:empty?) && @user_access_token.empty?)
|
57
|
+
|
58
|
+
if @token_info.all? { |_k, v| v.nil? }
|
59
|
+
token_info = request_token
|
60
|
+
save_token_info(
|
61
|
+
token_info: token_info
|
62
|
+
)
|
63
|
+
return @token_info["access_token"]
|
64
|
+
elsif token_expired?
|
65
|
+
token_info = refresh_token_expired? ? request_token : refresh_token
|
66
|
+
save_token_info(
|
67
|
+
token_info: token_info
|
68
|
+
)
|
69
|
+
return @token_info["access_token"]
|
70
|
+
else
|
71
|
+
@token_info["access_token"]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
# Request an IAM token using an API key
|
78
|
+
def request_token
|
79
|
+
headers = {
|
80
|
+
"Content-Type" => CONTENT_TYPE,
|
81
|
+
"Authorization" => DEFAULT_AUTHORIZATION,
|
82
|
+
"Accept" => ACCEPT
|
83
|
+
}
|
84
|
+
data = {
|
85
|
+
"grant_type" => REQUEST_TOKEN_GRANT_TYPE,
|
86
|
+
"apikey" => @iam_apikey,
|
87
|
+
"response_type" => REQUEST_TOKEN_RESPONSE_TYPE
|
88
|
+
}
|
89
|
+
response = request(
|
90
|
+
method: "POST",
|
91
|
+
url: @iam_url,
|
92
|
+
headers: headers,
|
93
|
+
data: data
|
94
|
+
)
|
95
|
+
response
|
96
|
+
end
|
97
|
+
|
98
|
+
# Refresh an IAM token using a refresh token
|
99
|
+
def refresh_token
|
100
|
+
headers = {
|
101
|
+
"Content-Type" => CONTENT_TYPE,
|
102
|
+
"Authorization" => DEFAULT_AUTHORIZATION,
|
103
|
+
"accept" => ACCEPT
|
104
|
+
}
|
105
|
+
data = {
|
106
|
+
"grant_type" => REFRESH_TOKEN_GRANT_TYPE,
|
107
|
+
"refresh_token" => @token_info["refresh_token"]
|
108
|
+
}
|
109
|
+
response = request(
|
110
|
+
method: "POST",
|
111
|
+
url: @iam_url,
|
112
|
+
headers: headers,
|
113
|
+
data: data
|
114
|
+
)
|
115
|
+
response
|
116
|
+
end
|
117
|
+
|
118
|
+
# Check if currently stored token is expired.
|
119
|
+
# Using a buffer to prevent the edge case of the
|
120
|
+
# token expiring before the request could be made.
|
121
|
+
# The buffer will be a fraction of the total TTL. Using 80%.
|
122
|
+
def token_expired?
|
123
|
+
return true if @token_info["expiration"].nil? || @token_info["expires_in"].nil?
|
124
|
+
|
125
|
+
fraction_of_ttl = 0.8
|
126
|
+
time_to_live = @token_info["expires_in"].nil? ? 0 : @token_info["expires_in"]
|
127
|
+
expire_time = @token_info["expiration"].nil? ? 0 : @token_info["expiration"]
|
128
|
+
refresh_time = expire_time - (time_to_live * (1.0 - fraction_of_ttl))
|
129
|
+
current_time = Time.now.to_i
|
130
|
+
refresh_time < current_time
|
131
|
+
end
|
132
|
+
|
133
|
+
# Used as a fail-safe to prevent the condition of a refresh token expiring,
|
134
|
+
# which could happen after around 30 days. This function will return true
|
135
|
+
# if it has been at least 7 days and 1 hour since the last token was set
|
136
|
+
def refresh_token_expired?
|
137
|
+
return true if @token_info["expiration"].nil?
|
138
|
+
|
139
|
+
seven_days = 7 * 24 * 3600
|
140
|
+
current_time = Time.now.to_i
|
141
|
+
new_token_time = @token_info["expiration"] + seven_days
|
142
|
+
new_token_time < current_time
|
143
|
+
end
|
144
|
+
|
145
|
+
# Save the response from the IAM service request to the object's state
|
146
|
+
def save_token_info(token_info:)
|
147
|
+
@token_info = token_info
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|