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 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,3 @@
1
+ # ruby-sdk-core
2
+ This project contains the core functionality used by Ruby SDK's generated by the IBM OpenAPI 3 SDK Generator (openapi-sdkgen).
3
+ Ruby code generated by openapi-sdkgen will depend on the classes contained in this project.
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Module for the IBM Cloud SDK Core
4
+ module IBMCloudSdkCore
5
+ require_relative("./ibm_cloud_sdk_core/base_service.rb")
6
+ end
@@ -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