ibm_cloud_sdk_core 0.1.1
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.
- 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
|