algosec-sdk 1.0.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.
@@ -0,0 +1,37 @@
1
+ # (c) Copyright 2018 AlgoSec Systems
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # You may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ #
7
+ # Unless required by applicable law or agreed to in writing, software distributed
8
+ # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
9
+ # CONDITIONS OF ANY KIND, either express or implied. See the License for the specific
10
+ # language governing permissions and limitations under the License.
11
+
12
+ # Contains all the custom Exception classes
13
+ module ALGOSEC_SDK
14
+ # Client configuration is invalid
15
+ class InvalidClient < StandardError
16
+ end
17
+
18
+ # Could not make request
19
+ class InvalidRequest < StandardError
20
+ end
21
+
22
+ # 400
23
+ class BadRequest < StandardError
24
+ end
25
+
26
+ # 401
27
+ class Unauthorized < StandardError
28
+ end
29
+
30
+ # 404
31
+ class NotFound < StandardError
32
+ end
33
+
34
+ # Other bad response codes
35
+ class RequestError < StandardError
36
+ end
37
+ end
@@ -0,0 +1,347 @@
1
+ require_relative 'flow_comparisons'
2
+ require 'set'
3
+ require 'ipaddress'
4
+
5
+ module ALGOSEC_SDK
6
+ module NetworkObjectType
7
+ HOST = 'Host'.freeze
8
+ RANGE = 'Range'.freeze
9
+ GROUP = 'Group'.freeze
10
+ ABSTRACT = 'Abstract'.freeze
11
+ end
12
+ end
13
+
14
+ module ALGOSEC_SDK
15
+ module NetworkObjectSearchType
16
+ INTERSECT = 'INTERSECT'.freeze
17
+ CONTAINED = 'CONTAINED'.freeze
18
+ CONTAINING = 'CONTAINING'.freeze
19
+ EXACT = 'EXACT'.freeze
20
+ end
21
+ end
22
+
23
+ module ALGOSEC_SDK
24
+ # Contains helper methods for BusinessFlow
25
+ module BusinessFlowHelper
26
+ # Request login to get session cookie credentials
27
+ # @raise [RuntimeError] if the request failed
28
+ # @return [Array<Hash>] flows
29
+ def login
30
+ response_handler(rest_post('/BusinessFlow/rest/v1/login'))
31
+ end
32
+
33
+ # Get list of application flows for an application revision id
34
+ # @param [String, Symbol] app_revision_id
35
+ # @raise [RuntimeError] if the request failed
36
+ # @return [Array<Hash>] flows
37
+ def get_application_flows(app_revision_id)
38
+ response = rest_get("/BusinessFlow/rest/v1/applications/#{app_revision_id}/flows")
39
+ flows = response_handler(response)
40
+ flows.map { |flow| flow['flowType'] == 'APPLICATION_FLOW' ? flow : nil }.compact
41
+ end
42
+
43
+ # Get application flows from the server as a hash from flow name it it's content
44
+ # @param [String, Symbol] app_revision_id
45
+ # @raise [RuntimeError] if the request failed
46
+ # @return [Hash] flows as a hash from name to flow
47
+ def get_application_flows_hash(app_revision_id)
48
+ Hash[get_application_flows(app_revision_id).map { |flow| [flow['name'], flow] }]
49
+ end
50
+
51
+ # Delete a specific flow
52
+ # @param [String] app_revision_id
53
+ # @param [String] flow_id
54
+ # @raise [RuntimeError] if the request failed
55
+ # @return true
56
+ def delete_flow_by_id(app_revision_id, flow_id)
57
+ response = rest_delete("/BusinessFlow/rest/v1/applications/#{app_revision_id}/flows/#{flow_id}")
58
+ response_handler(response)
59
+ true
60
+ end
61
+
62
+ # Get connectivity status for a flow
63
+ # @param [String] app_revision_id
64
+ # @param [String] flow_id
65
+ # @raise [RuntimeError] if the request failed
66
+ # @return [String] Connectivity Status dict that contain flowId, queryLink and status keys
67
+ def get_flow_connectivity(app_revision_id, flow_id)
68
+ response = rest_post("/BusinessFlow/rest/v1/applications/#{app_revision_id}/flows/#{flow_id}/check_connectivity")
69
+ response_handler(response)
70
+ end
71
+
72
+ # Create a flow
73
+ # @param [String] app_revision_id The application revision id to create the flow in
74
+ # @param [Object] flow_name
75
+ # @param [Array<String>] sources
76
+ # @param [Array<String>] destinations
77
+ # @param [Array<String>] network_users
78
+ # @param [Array<String>] network_apps
79
+ # @param [Array<String>] network_services
80
+ # @param [String] comment
81
+ # @param [String] type
82
+ # @raise [RuntimeError] if the request failed
83
+ # @return Newly created application flow
84
+ # rubocop:disable Metrics/ParameterLists
85
+ def create_application_flow(
86
+ app_revision_id,
87
+ flow_name,
88
+ sources,
89
+ destinations,
90
+ network_services,
91
+ network_users,
92
+ network_apps,
93
+ comment,
94
+ type = 'APPLICATION',
95
+ custom_fields = []
96
+ )
97
+ # rubocop:enable Metrics/ParameterLists
98
+
99
+ # Create the missing network objects from the sources and destinations
100
+ create_missing_network_objects(sources + destinations)
101
+ create_missing_services(network_services)
102
+
103
+ get_named_objects = ->(name_list) { name_list.map { |name| { name: name } } }
104
+
105
+ new_flow = {
106
+ name: flow_name,
107
+ sources: get_named_objects.call(sources),
108
+ destinations: get_named_objects.call(destinations),
109
+ users: network_users,
110
+ network_applications: get_named_objects.call(network_apps),
111
+ services: get_named_objects.call(network_services),
112
+ comment: comment,
113
+ type: type,
114
+ custom_fields: custom_fields
115
+ }
116
+ response = rest_post("/BusinessFlow/rest/v1/applications/#{app_revision_id}/flows/new", body: [new_flow])
117
+ flows = response_handler(response)
118
+ # AlgoSec return a list of created flows, we created only one
119
+ flows[0]
120
+ end
121
+
122
+ # Fetch an application flow by it's name
123
+ # @param [String] app_revision_id The application revision id to fetch the flow from
124
+ # @param [Object] flow_name
125
+ # @raise [RuntimeError] if the request failed
126
+ # @return The requested flow
127
+ def get_application_flow_by_name(app_revision_id, flow_name)
128
+ flows = get_application_flows(app_revision_id)
129
+ requested_flow = flows.find do |flow|
130
+ break flow if flow['name'] == flow_name
131
+ end
132
+
133
+ if requested_flow.nil?
134
+ raise(
135
+ "Unable to find flow by name. Application revision id: #{app_revision_id}, flow_name: #{flow_name}."
136
+ )
137
+ end
138
+ requested_flow
139
+ end
140
+
141
+ # Get latest application revision id by application name
142
+ # @param [String, Symbol] app_name
143
+ # @raise [RuntimeError] if the request failed
144
+ # @return [Boolean] application revision id
145
+ def get_app_revision_id_by_name(app_name)
146
+ response = rest_get("/BusinessFlow/rest/v1/applications/name/#{app_name}")
147
+ app = response_handler(response)
148
+ app['revisionID']
149
+ end
150
+
151
+ # Apply application draft
152
+ # @param [String] app_revision_id
153
+ # @raise [RuntimeError] if the request failed
154
+ # @return true
155
+ def apply_application_draft(app_revision_id)
156
+ response = rest_post("/BusinessFlow/rest/v1/applications/#{app_revision_id}/apply")
157
+ response_handler(response)
158
+ true
159
+ end
160
+
161
+ # Create a new network service
162
+ # @param [String] service_name
163
+ # @param content List of lists in the form of (protocol, port)
164
+ # @raise [RuntimeError] if the request failed
165
+ # @return true if service created or already exists
166
+ def create_network_service(service_name, content)
167
+ content = content.map { |service| { protocol: service[0], port: service[1] } }
168
+ new_service = { name: service_name, content: content }
169
+ response = rest_post('/BusinessFlow/rest/v1/network_services/new', body: new_service)
170
+ response_handler(response)
171
+ true
172
+ end
173
+
174
+ # Create a new network object
175
+ # @param [NetworkObjectType] type type of the object to be created
176
+ # @param [String] content Define the newly created network object. Content depend upon the selected type
177
+ # @param [String] name Name of the new network object
178
+ # @raise [RuntimeError] if the request failed
179
+ # @return Newly created object
180
+ def create_network_object(type, content, name)
181
+ new_object = { type: type, name: name, content: content }
182
+ response = rest_post('/BusinessFlow/rest/v1/network_objects/new', body: new_object)
183
+ response_handler(response)
184
+ end
185
+
186
+ # Search a network object
187
+ # @param [String] ip_or_subnet The ip or subnet to search the object with
188
+ # @param [NetworkObjectSearchType] search_type type of the object search method
189
+ # @raise [RuntimeError] if theh request failed
190
+ # @return List of objects from the search result
191
+ def search_network_object(ip_or_subnet, search_type)
192
+ response = rest_get(
193
+ '/BusinessFlow/rest/v1/network_objects/find',
194
+ query: { address: ip_or_subnet, type: search_type }
195
+ )
196
+ response_handler(response)
197
+ end
198
+
199
+ # Return a plan for modifying application flows based on current and newly proposed application flows definition
200
+ # @param [Array<Hash>] server_app_flows List of app flows currently defined on the server
201
+ # @param [Array<Hash>] new_app_flows List of network flows hash definitions
202
+ # @raise [RuntimeError] if the request failed
203
+ # @return 3 lists of flow names: flows_to_delete, flows_to_create, flows_to_modify
204
+ def plan_application_flows(server_app_flows, new_app_flows)
205
+ current_flow_names = Set.new(server_app_flows.keys)
206
+ new_flow_names = Set.new(new_app_flows.keys)
207
+ # Calculate the flows_to_delete, flows_to_create and flows_to_modify and unchanging_flows
208
+ flows_to_delete = current_flow_names - new_flow_names
209
+ flows_to_create = new_flow_names - current_flow_names
210
+ flows_to_modify = Set.new((new_flow_names & current_flow_names).map do |flow_name|
211
+ flow_on_server = server_app_flows[flow_name]
212
+ new_flow_definition = new_app_flows[flow_name]
213
+ ALGOSEC_SDK::AreFlowsEqual.flows_equal?(new_flow_definition, flow_on_server) ? nil : flow_name
214
+ end.compact)
215
+
216
+ [flows_to_delete, flows_to_create, flows_to_modify]
217
+ end
218
+
219
+ # Create/modify/delete application2 flows to match a given flow plan returned by 'plan_application_flows'
220
+ # @param [Integer] app_name The app to create the flows for
221
+ # @param [Array<Hash>] new_app_flows List of network flows hash definitions
222
+ # @param [Array<Hash>] flows_from_server List of network flows objects fetched from the server
223
+ # @param [Array<String>] flows_to_delete List of network flow names for deletion
224
+ # @param [Array<String>] flows_to_create List of network flow names to create
225
+ # param [Array<String>] flows_to_modify List of network flow names to delete and re-create with the new definition
226
+ # @raise [RuntimeError] if any of the requests failed
227
+ # @return True
228
+ def implement_app_flows_plan(
229
+ app_name,
230
+ new_app_flows,
231
+ flows_from_server,
232
+ flows_to_delete,
233
+ flows_to_create,
234
+ flows_to_modify
235
+ )
236
+ # Get the app revision id
237
+ app_revision_id = get_app_revision_id_by_name(app_name)
238
+
239
+ # This param is used to determine if it is necessary to update the app_revision_id
240
+ is_draft_revision = false
241
+
242
+ # Delete all the flows for deletion and modification
243
+ (flows_to_delete | flows_to_modify).each do |flow_name_to_delete|
244
+ delete_flow_by_id(app_revision_id, flows_from_server[flow_name_to_delete]['flowID'])
245
+ next if is_draft_revision
246
+ app_revision_id = get_app_revision_id_by_name(app_name)
247
+ # Refetch the fresh flows from the server, as a new application revision has been created
248
+ # and it's flow IDs have been change. Only that way we can make sure that the following flow deletions
249
+ # by name will work as expected
250
+ flows_from_server = get_application_flows(app_revision_id)
251
+ is_draft_revision = true
252
+ end
253
+ # Create all the new + modified flows
254
+ (flows_to_create | flows_to_modify).each do |flow_name_to_create|
255
+ new_flow_data = new_app_flows[flow_name_to_create]
256
+ create_application_flow(
257
+ app_revision_id,
258
+ flow_name_to_create,
259
+ # Document those key fields somewhere so users know how what is the format of app_flows object
260
+ # that is provided to this function
261
+ new_flow_data['sources'],
262
+ new_flow_data['destinations'],
263
+ new_flow_data['services'],
264
+ new_flow_data.fetch('users', []),
265
+ new_flow_data.fetch('applications', []),
266
+ new_flow_data.fetch('comment', '')
267
+ )
268
+ unless is_draft_revision
269
+ app_revision_id = get_app_revision_id_by_name(app_name)
270
+ is_draft_revision = true
271
+ end
272
+ end
273
+
274
+ apply_application_draft(app_revision_id) if is_draft_revision
275
+ end
276
+
277
+ # Update application flows of an application to match a requested flows configuration.
278
+ # @param [Integer] app_name The app to create the flows for
279
+ # @param [Object] new_app_flows Hash of new app flows, pointing from the flow name to the flow definition
280
+ # @raise [RuntimeError] if the request failed
281
+ # @return The updated list of flow objects from the server, including their new flowID
282
+ def define_application_flows(app_name, new_app_flows)
283
+ flows_from_server = get_application_flows_hash(get_app_revision_id_by_name(app_name))
284
+ flows_to_delete, flows_to_create, flows_to_modify = plan_application_flows(flows_from_server, new_app_flows)
285
+ implement_app_flows_plan(
286
+ app_name,
287
+ new_app_flows,
288
+ flows_from_server,
289
+ flows_to_delete,
290
+ flows_to_create,
291
+ flows_to_modify
292
+ )
293
+
294
+ # Stage 2: Run connectivity check for all the unchanged flows. Check with Chef is this non-deterministic approach
295
+ # is OK with them for the cookbook.
296
+ #
297
+ # Return the current list of created flows if successful
298
+ get_application_flows(get_app_revision_id_by_name(app_name))
299
+ end
300
+
301
+ # Create all the missing network objects which are simple IPv4 ip or subnet
302
+ # @param [Array<String>] network_object_names List of the network object names
303
+ # @raise [RuntimeError] if the request failed
304
+ # @return Newly created objects
305
+ def create_missing_network_objects(network_object_names)
306
+ # TODO: Add unitests that objects are being create only once (if the same object is twice in the incoming list)
307
+ network_object_names = Set.new(network_object_names)
308
+ ipv4_or_subnet_objects = network_object_names.map do |object_name|
309
+ begin
310
+ IPAddress.parse object_name
311
+ search_result = search_network_object(object_name, NetworkObjectSearchType::EXACT)
312
+ # If no object was found in search, we'll count this object for creation
313
+ search_result.empty? ? object_name : nil
314
+ rescue ArgumentError
315
+ # The parsed object name was not IP Address or IP Subnet, ignore it
316
+ nil
317
+ end
318
+ end.compact
319
+
320
+ # Create all the objects. If the error from the server tells us that the object already exists, ignore the error
321
+ ipv4_or_subnet_objects.map do |ipv4_or_subnet|
322
+ create_network_object(NetworkObjectType::HOST, ipv4_or_subnet, ipv4_or_subnet)
323
+ end.compact
324
+ end
325
+
326
+ # Create all the missing network services which are of simple protocol/port pattern
327
+ # @param [Array<String>] service_names List of the network service names
328
+ # @raise [RuntimeError] if the request failed
329
+ # @return Newly created objects
330
+ def create_missing_services(service_names)
331
+ parsed_services = service_names.map do |service_name|
332
+ protocol, port = service_name.scan(%r{(TCP|UDP)/(\d+)}i).last
333
+ [service_name, [protocol, port]] if !protocol.nil? && !port.nil?
334
+ end.compact
335
+ # Create all the objects. If the error from the server tells us that the object already exists, ignore the error
336
+ parsed_services.map do |parsed_service|
337
+ service_name, service_content = parsed_service
338
+ begin
339
+ create_network_service(service_name, [service_content])
340
+ rescue StandardError => e
341
+ # If the error is different from "service already exists", the exception will be re-raised
342
+ raise e if e.to_s.index('Service name already exists').nil?
343
+ end
344
+ end.compact
345
+ end
346
+ end
347
+ end
@@ -0,0 +1,48 @@
1
+ require 'set'
2
+
3
+ module ALGOSEC_SDK
4
+ ANY_OBJECT = { 'id' => 0, 'name' => 'Any' }.freeze
5
+ ANY_NETWORK_APPLICATION = { 'revisionID' => 0, 'name' => 'Any' }.freeze
6
+ # A module to determine if a local flow definition is equal to a flow defined on the server
7
+ module AreFlowsEqual
8
+ def self.are_sources_equal_in_flow(source_object_names, server_flow_sources)
9
+ flow_source_object_names = Set.new(server_flow_sources.map { |source| source['name'] })
10
+ Set.new(source_object_names) == Set.new(flow_source_object_names)
11
+ end
12
+
13
+ def self.are_dest_equal_in_flow(dest_object_names, server_flow_dests)
14
+ flow_dest_object_names = Set.new(server_flow_dests.map { |dest| dest['name'] })
15
+ Set.new(dest_object_names) == Set.new(flow_dest_object_names)
16
+ end
17
+
18
+ def self.are_services_equal_in_flow(service_names, server_flow_services)
19
+ network_flow_service_names = Set.new(server_flow_services.map { |service| service['name'] })
20
+ Set.new(service_names) == Set.new(network_flow_service_names)
21
+ end
22
+
23
+ def self.are_apps_equal_in_flow(application_names, server_flow_apps)
24
+ return application_names == [] if server_flow_apps == [ANY_NETWORK_APPLICATION]
25
+ flow_application_names = server_flow_apps.map do |network_application|
26
+ network_application['name']
27
+ end
28
+
29
+ Set.new(application_names) == Set.new(flow_application_names)
30
+ end
31
+
32
+ def self.are_users_equal_in_flow(network_users, server_flow_users)
33
+ return network_users == [] if server_flow_users == [ANY_OBJECT]
34
+ flow_users = server_flow_users.map { |user| user['name'] }
35
+ Set.new(network_users) == Set.new(flow_users)
36
+ end
37
+
38
+ def self.flows_equal?(new_flow, flow_from_server)
39
+ [
40
+ are_sources_equal_in_flow(new_flow['sources'], flow_from_server['sources']),
41
+ are_dest_equal_in_flow(new_flow['destinations'], flow_from_server['destinations']),
42
+ are_services_equal_in_flow(new_flow['services'], flow_from_server['services']),
43
+ are_apps_equal_in_flow(new_flow.fetch('applications', []), flow_from_server.fetch('networkApplications', [])),
44
+ are_users_equal_in_flow(new_flow.fetch('users', []), flow_from_server.fetch('networkUsers', []))
45
+ ].all?
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,160 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+ require 'openssl'
4
+ require 'json'
5
+ require 'jsonclient'
6
+
7
+ module ALGOSEC_SDK
8
+ # Adds the ability for httpclient to set proper content-type for Hash AND Array body
9
+ class AdvancedJSONClient < JSONClient
10
+ def argument_to_hash_for_json(args)
11
+ hash = argument_to_hash(args, :body, :header, :follow_redirect)
12
+ if hash[:body].is_a?(Hash) || hash[:body].is_a?(Array)
13
+ hash[:header] = json_header(hash[:header])
14
+ hash[:body] = JSON.generate(hash[:body])
15
+ end
16
+ hash
17
+ end
18
+ end
19
+ end
20
+
21
+ module ALGOSEC_SDK
22
+ # Contains all the methods for making API REST calls
23
+ module Rest
24
+ def init_http_client
25
+ @http_client = ALGOSEC_SDK::AdvancedJSONClient.new(force_basic_auth: true)
26
+ @http_client.proxy = nil if @disable_proxy
27
+ @http_client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE unless @ssl_enabled
28
+ @http_client.set_auth(@host, @user, @password)
29
+ end
30
+
31
+ # Make a restful API request to the AlgoSec
32
+ # @param [Symbol] type the rest method/type Options are :get, :post, :put, :patch, and :delete
33
+ # @param [String] path the path for the request. Usually starts with "/rest/"
34
+ # @param [Hash] options the options for the request
35
+ # @option options [String] :body Hash to be converted into json and set as the request body
36
+ # @option options [String] :Content-Type ('application/json') Set to nil or :none to have this option removed
37
+ # @raise [InvalidRequest] if the request is invalid
38
+ # @raise [SocketError] if a connection could not be made
39
+ # @raise [OpenSSL::SSL::SSLError] if SSL validation of the AlgoSec's certificate failed
40
+ # @return [NetHTTPResponse] The response object
41
+ def rest_api(type, path, options = {})
42
+ raise InvalidRequest, 'Must specify path' unless path
43
+ raise InvalidRequest, 'Must specify type' unless type
44
+ @logger.debug "Making :#{type} rest call to #{@host}#{path}"
45
+
46
+ uri = "#{@host}#{path}"
47
+ response = send_request(type, uri, options)
48
+ @logger.debug " Response: Code=#{response.status}. Headers=#{response.headers}\n Body=#{response.body}"
49
+ response
50
+ rescue OpenSSL::SSL::SSLError => e
51
+ msg = 'SSL verification failed for the request. Please either:'
52
+ msg += "\n 1. Install the necessary certificate(s) into your cert store"
53
+ msg += ". Using cert store: #{ENV['SSL_CERT_FILE']}" if ENV['SSL_CERT_FILE']
54
+ msg += "\n 2. Set the :ssl_enabled option to false for your AlgoSec client (not recommended)"
55
+ @logger.error msg
56
+ raise e
57
+ rescue SocketError => e
58
+ msg = "Failed to connect to AlgoSec host #{@host}!\n"
59
+ @logger.error msg
60
+ e.message.prepend(msg)
61
+ raise e
62
+ end
63
+
64
+ # Make a restful GET request
65
+ # Parameters & return value align with those of the {ALGOSEC_SDK::Rest::rest_api} method above
66
+ def rest_get(path, options = {})
67
+ rest_api(:get, path, options)
68
+ end
69
+
70
+ # Make a restful POST request
71
+ # Parameters & return value align with those of the {ALGOSEC_SDK::Rest::rest_api} method above
72
+ def rest_post(path, options = {})
73
+ rest_api(:post, path, options)
74
+ end
75
+
76
+ # Make a restful PUT request
77
+ # Parameters & return value align with those of the {ALGOSEC_SDK::Rest::rest_api} method above
78
+ def rest_put(path, options = {})
79
+ rest_api(:put, path, options)
80
+ end
81
+
82
+ # Make a restful PATCH request
83
+ # Parameters & return value align with those of the {ALGOSEC_SDK::Rest::rest_api} method above
84
+ def rest_patch(path, options = {})
85
+ rest_api(:patch, path, options)
86
+ end
87
+
88
+ # Make a restful DELETE request
89
+ # Parameters & return value align with those of the {ALGOSEC_SDK::Rest::rest_api} method above
90
+ def rest_delete(path, options = {})
91
+ rest_api(:delete, path, options)
92
+ end
93
+
94
+ RESPONSE_CODE_OK = 200
95
+ RESPONSE_CODE_CREATED = 201
96
+ RESPONSE_CODE_ACCEPTED = 202
97
+ RESPONSE_CODE_NO_CONTENT = 204
98
+ RESPONSE_CODE_BAD_REQUEST = 400
99
+ RESPONSE_CODE_UNAUTHORIZED = 401
100
+ RESPONSE_CODE_NOT_FOUND = 404
101
+
102
+ # Handle the response for rest call.
103
+ # If an asynchronous task was started, this waits for it to complete.
104
+ # @param [HTTPResponse] response
105
+ # @raise [ALGOSEC_SDK::BadRequest] if the request failed with a 400 status
106
+ # @raise [ALGOSEC_SDK::Unauthorized] if the request failed with a 401 status
107
+ # @raise [ALGOSEC_SDK::NotFound] if the request failed with a 404 status
108
+ # @raise [ALGOSEC_SDK::RequestError] if the request failed with any other status
109
+ # @return [Hash] The parsed JSON body
110
+ def response_handler(response)
111
+ case response.status
112
+ when RESPONSE_CODE_OK # Synchronous read/query
113
+ response.body
114
+ when RESPONSE_CODE_CREATED # Synchronous add
115
+ response.body
116
+ # when RESPONSE_CODE_ACCEPTED # Asynchronous add, update or delete
117
+ # return response.body #
118
+ # @logger.debug "Waiting for task: #{response.headers['location']}"
119
+ # task = wait_for(response.headers['location'])
120
+ # return true unless task['associatedResource'] && task['associatedResource']['resourceUri']
121
+ # resource_data = rest_get(task['associatedResource']['resourceUri'])
122
+ # return JSON.parse(resource_data.body)
123
+ when RESPONSE_CODE_NO_CONTENT # Synchronous delete
124
+ {}
125
+ when RESPONSE_CODE_BAD_REQUEST
126
+ raise BadRequest, "400 BAD REQUEST #{response.body}"
127
+ when RESPONSE_CODE_UNAUTHORIZED
128
+ raise Unauthorized, "401 UNAUTHORIZED #{response.body}"
129
+ when RESPONSE_CODE_NOT_FOUND
130
+ raise NotFound, "404 NOT FOUND #{response.body}"
131
+ else
132
+ raise RequestError, "#{response.status} #{response.body}"
133
+ end
134
+ end
135
+
136
+ private
137
+
138
+ # @param type [Symbol] The type of request object to build (get, post, put, patch, or delete)
139
+ # @param uri [String] full URI string
140
+ # @param options [Hash] Options for building the request. All options except "body" are set as headers.
141
+ # @raise [ALGOSEC_SDK::InvalidRequest] if the request type is not recognized
142
+ def send_request(type, uri, options)
143
+ case type.downcase
144
+ when 'get', :get
145
+ response = @http_client.get(uri, options)
146
+ when 'post', :post
147
+ response = @http_client.post(uri, options)
148
+ when 'put', :put
149
+ response = @http_client.put(uri, options)
150
+ when 'patch', :patch
151
+ response = @http_client.patch(uri, options)
152
+ when 'delete', :delete
153
+ response = @http_client.delete(uri, options)
154
+ else
155
+ raise InvalidRequest, "Invalid rest method: #{type}. Valid methods are: get, post, put, patch, delete"
156
+ end
157
+ response
158
+ end
159
+ end
160
+ end