razorrisk-razor-connectivity 0.14.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,559 @@
1
+ # encoding: UTF-8
2
+
3
+ # ######################################################################## #
4
+ #
5
+ # Class for connecting to the Razor application via the RazorRequest
6
+ # executable.
7
+ #
8
+ # Copyright (c) 2017 Razor Risk Technologies Pty Limited. All rights reserved.
9
+ #
10
+ # ######################################################################## #
11
+
12
+
13
+ # ######################################################################## #
14
+ # requires
15
+
16
+ require 'razor_risk/razor/connectivity/razor_3/response'
17
+ require 'razor_risk/razor/connectivity/exceptions'
18
+
19
+ require 'razor_risk/core/system/system_traits'
20
+
21
+ require 'razor_risk/core/diagnostics/logger'
22
+
23
+ require 'nokogiri'
24
+ require 'pantheios'
25
+ require 'recls'
26
+ require 'xqsr3/quality/parameter_checking'
27
+ require 'xqsr3/extensions/string/quote_if'
28
+
29
+ require 'open3'
30
+ require 'tempfile'
31
+
32
+ # ######################################################################## #
33
+ # modules
34
+
35
+ module RazorRisk
36
+ module Razor
37
+ module Connectivity
38
+ module Razor3
39
+
40
+ # ######################################################################## #
41
+ # classes
42
+
43
+ # Class that provides easy call interface to Razor, following along with the
44
+ # "traditional" behaviour of the RazorRequest tool
45
+ #
46
+ # It is used by creating an instance, which requires the ClarITe config
47
+ # (path) and the Razor Environment name, as in:
48
+ #
49
+ # +rr = RazorRequester.new cc_path, env_name
50
+ #
51
+ # and then requests are dispatched to the relevant Razor installation (as
52
+ # named by the environment) via the +send_request+ method, as in:
53
+ #
54
+ # +rr.send_request request
55
+ #
56
+ # where +request+ is a Razor request document.
57
+ #
58
+ # Both initialiser and +send_request+ methods take options, as detailed in
59
+ # their respective documentation sections. The initialiser's +:executable+
60
+ # option is required when the class method +requires_executable?+ returns
61
+ # +true+, and should point to the full path of the underlying RazorRequest
62
+ # program to be used for connectivity with the Razor system
63
+ class RazorRequester
64
+
65
+ include ::Pantheios
66
+ include ::Xqsr3::Quality::ParameterChecking
67
+ include ::RazorRisk::Core::Diagnostics::Logger
68
+ private
69
+ def nil_if_empty v
70
+
71
+ return nil if v.respond_to?(:empty?) && v.empty?
72
+ v
73
+ end
74
+ def to_nil *args; end
75
+ def self.to_nil *args; end
76
+ public
77
+
78
+ # FIXME: THIS MUST BE FIXED.
79
+ #
80
+ # Validate that all characters in the string are alphanumerics, spaces,
81
+ # or underscores.
82
+ #
83
+ # This check is temporary and does not grantee SQL safety. This
84
+ # must be addressed as a larger issue around sanitization of user
85
+ # inputs to Razor.
86
+ #
87
+ # @param str [String] The string to be validated.
88
+ #
89
+ # @yield Executes the block if the string is not safe.
90
+ #
91
+ # @raise [ArgumentError] If the argument is not a string.
92
+ # @raise [ArgumentError] If the argument is not a string.
93
+ #
94
+ # @example Raise an exception if unsafe.
95
+ # stopgap_sql_string_validator_fix_me '.!?' { raise ArgumentError }
96
+ def self.stopgap_sql_string_validator_fix_me str, &block
97
+
98
+ raise ArgumentError.new 'No block was given' unless block_given?
99
+
100
+ check_parameter str, 'str', type: ::String
101
+
102
+ unless /^[A-Za-z0-9_ ]+$/ =~ str
103
+ yield
104
+ end
105
+ end
106
+
107
+ # see #stopgap_sql_string_validator_fix_me
108
+ def stopgap_sql_string_validator_fix_me *args, &block
109
+ self.class.stopgap_sql_string_validator_fix_me(*args, &block)
110
+ end
111
+
112
+ # ##########################################################
113
+ # types
114
+
115
+ # Root exception for failures of RazorRequester
116
+ class Exception < ::RazorRisk::Razor::Connectivity::Exceptions::ConnectivityException
117
+ end
118
+
119
+ # Exception thrown when request fails
120
+ class RequestFailedException < Exception
121
+
122
+ def initialize message, exit_status, razor_code, contingent_report, **options
123
+
124
+ trace ParamNames[ :message, :exit_status, :razor_code, :contingent_report, :options ], message, exit_status, razor_code, contingent_report, options
125
+
126
+ if (message || '').empty?
127
+
128
+ message = contingent_report
129
+ else
130
+
131
+ if (contingent_report || '').empty?
132
+
133
+ ;
134
+ else
135
+
136
+ message = "#{message}: #{contingent_report}"
137
+ end
138
+ end
139
+
140
+ super message, options
141
+
142
+ @exit_status = exit_status
143
+ @razor_code = razor_code
144
+ @contingent_report = contingent_report
145
+ end
146
+
147
+ attr_reader :exit_status
148
+ attr_reader :razor_code
149
+ attr_reader :contingent_report
150
+
151
+ def inspect
152
+
153
+ "#<#{self.class}:0x00#{object_id << 1}: exit_status=#{exit_status}; razor_code=#{razor_code}; contingent_report=#{contingent_report}>"
154
+ end
155
+ end
156
+
157
+ # Raised when the response from the +:razor_executable+ is not
158
+ # recognised (which might mean an invalid executable has been specified)
159
+ class InvalidResponseException < Exception; end
160
+
161
+ # Raised when the credentials given are invalid
162
+ class InvalidCredentialsException < RequestFailedException; end
163
+
164
+ # Represents the executable
165
+ class Executable
166
+
167
+ include RazorRisk::Core::System::SystemTraits
168
+
169
+ include ::Xqsr3::Quality::ParameterChecking
170
+ include ::Xqsr3::Quality::ParameterChecking
171
+
172
+ module Constants
173
+
174
+ RRParameters = {
175
+ username: '/usr',
176
+ password: '/pwd',
177
+ domain: '/domain',
178
+ impersonatee: '/impersonate',
179
+ razor_space: '/space',
180
+ razor_environment: '/env',
181
+ razor_alias: '/alias',
182
+ session_id: '/sessionId',
183
+ }
184
+
185
+ RROptions = {
186
+ debug: '/debug',
187
+ logStderr: '/logStderr',
188
+ session: '/session',
189
+ open_session: '/openSession',
190
+ }
191
+ end
192
+
193
+ # Initialises the instance
194
+ #
195
+ # @param executable [::String, Array<::String>] As string with 1 or
196
+ # more elements separated by '|' (or by the platform's separator -
197
+ # ';' on Windows; ':' otherwise)
198
+ #
199
+ def initialize executable
200
+
201
+ check_parameter executable, 'executable', types: [ ::String, [ ::String ] ]
202
+
203
+ exec_parts = executable.split(windows? ? /[|;]/ : /[|:]/) if ::String === executable
204
+ exec_parts = exec_parts.flatten
205
+ exec_parts = exec_parts.reject { |xp| xp.strip.empty? }
206
+
207
+ raise ::ArgumentError, "the given executable '#{executable}' resolves to nothing" if exec_parts.empty?
208
+
209
+ arg0 = exec_parts.shift
210
+
211
+ @arg0 = Recls.stat(arg0) or raise ArgumentError, "given executable '#{arg0}' does not exist"
212
+ @argv = exec_parts
213
+ end
214
+
215
+ # Forms the full command to be executed, based on the given
216
+ # parameters, options, and also on the nature of the executable
217
+ # specified in the initialiser
218
+ #
219
+ # @param config_path [::String] The ClarITe configuration
220
+ # @param request_path [::String] The path of the request document.
221
+ # @param options [::Hash] Options hash
222
+ #
223
+ # @option options [::String] :username The user to use fro the
224
+ # credentials.
225
+ # @option options [::String] :password The password to use for
226
+ # credentials.
227
+ # @option options [::String] :domain The domain to use for
228
+ # credentials.
229
+ # @option options [::String] :impersonatee The user to impersonate.
230
+ # @option options [::String] :razor_environment The Razor environment.
231
+ # @option options [Boolean] :session Uses a Razor session for the
232
+ # request.
233
+ # @option options [Boolean] :debug Turns on debug.
234
+ # @option options [Boolean] :log_to_stderr Turns on logging.
235
+ #
236
+ # @return [::String] The command to execute.
237
+ def form_command config_path, request_path, **options
238
+
239
+ cmd = []
240
+ cmd << program_path.to_s.quote_if
241
+
242
+ unless program_arguments.empty?
243
+
244
+ program_arguments.each do |arg|
245
+
246
+ cmd << arg.quote_if
247
+ end
248
+ else
249
+
250
+ cmd << '/config' << config_path.quote_if
251
+ cmd << '/request' << request_path.quote_if unless request_path.nil?
252
+
253
+ Constants::RRParameters.each do |key,value|
254
+ cmd << value << options[key].quote_if if options[key]
255
+ end
256
+
257
+ Constants::RROptions.each do |key,value|
258
+ cmd << value if options[key]
259
+ end
260
+ end
261
+
262
+ cmd * ' '
263
+ end
264
+
265
+ def program_path; @arg0; end
266
+ def program_arguments; @argv; end
267
+ end
268
+
269
+ # Indicates whether the instance requires an executable
270
+ def self.requires_executable?
271
+
272
+ true
273
+ end
274
+
275
+ # Initialises an instance with configuration and set-up options
276
+ #
277
+ # @param config [::String, nil] The ClarITe configuration. By default
278
+ # this is interpreted as a file path, unless the option
279
+ # +:config_as_string+ is given. If +nil+ it will be obtained from
280
+ # option +:config+. One or the other must be supplied.
281
+ # @param options [::Hash] The options hash.
282
+ #
283
+ # @option options [::String] :config The ClarITe configuration. By
284
+ # default this is interpreted as a file path, unless the option
285
+ # +:config_as_string+ is given. Ignored if the parameter +config+ is
286
+ # not +nil+.
287
+ # @option options [::String] :razor_environment Specifies the Razor
288
+ # environment. May not be combinbed with +:razor_alias+.
289
+ # @option options [::String] :razor_space Specifies the ClarITe
290
+ # space. May not be combinbed with +:razor_alias+.
291
+ # @option options [::String] :razor_alias Specifies the Razor alias. May
292
+ # not be combinbed with +:razor_environment+ or +:razor_space+.
293
+ # @option options [Boolean] :config_as_string The +config+ parameter
294
+ # [or +:config+ option] is treated as a string containing the
295
+ # configuration, rather than the path to a configuration file.
296
+ # @option options [::String] :executable Specifies the executable for
297
+ # RazorRequest. Required if the class method +requires_executable?+ is
298
+ # +true+. May contain program arguments, separated using the
299
+ # platform-independent path separator '|' [or the platform-specific
300
+ # path separator - ';' for Windows; ':' otherwise].
301
+ # @option options :aborter DEPRECATED
302
+ #
303
+ def initialize config, **options
304
+
305
+ trace ParamNames[ :config, :options ], config, options
306
+
307
+ check_parameter config, 'config', type: ::String, allow_nil: true
308
+
309
+ warn ':aborter option is not longer recognised' if options[:aborter]
310
+
311
+ # NOTE: This (ctor) method concerns itself only with specifying (and
312
+ # validating) the executable and the configuration
313
+
314
+ config = nil_if_empty(config) || nil_if_empty(options[:config]) or raise ArgumentError, 'config not specified'
315
+
316
+ unless options[:config_as_string]
317
+ raise ArgumentError, "given config file '#{config}' does not exist, or is not a file" unless File.file? config
318
+ end
319
+
320
+ unless options[:razor_environment] or options[:razor_alias]
321
+ raise ArgumentError, 'either :razor_environment or :razor_alias options must be specified'
322
+ end
323
+
324
+ if self.class.requires_executable?
325
+
326
+ executable = options[:executable] or raise ArgumentError, ':executable option is required'
327
+
328
+ @executable = Executable.new executable
329
+ end
330
+
331
+ @config = config
332
+ @options = options
333
+ end
334
+
335
+ attr_reader :config
336
+ attr_reader :options
337
+
338
+ def executable
339
+ @executable.program_path
340
+ end
341
+
342
+ def environment
343
+ @options[:razor_environment] || @options[:razor_alias]
344
+ end
345
+
346
+ # Sends the given request.
347
+ #
348
+ # @param request [#to_s] The string form of this object is used as the
349
+ # request.
350
+ # @param options [::Hash] The options hash.
351
+ #
352
+ # @option options [::String] :username The username part of the
353
+ # credentials.
354
+ # @option options [::String] :password The password to be specified as
355
+ # part of the credentials. Ignored unless +options[:username]+ is
356
+ # specified.
357
+ # @option options [::String] :domain The domain to be specified as part
358
+ # of the credentials. Ignored unless options +:username]+ is specified.
359
+ # @option options [::String] :impersonatee The name of the account that
360
+ # will be impersonated for the purposes of the request.
361
+ # @option options [Boolean] :session Causes a Razor session to be used
362
+ # for the request.
363
+ # @option options [Boolean] :request_as_path The +request+ parameter is
364
+ # treated as a path to a file containing the request contents, rather
365
+ # than the request contents.
366
+ # @option options [Boolean] :debug Causes certain debugging actions,
367
+ # including not removing temporary files.
368
+ # @option options [Boolean] :log_to_stderr Causes the RazorRequest CLI
369
+ # flag '/logStderr' to be included in the command-line.
370
+ # @option options :request_as_string DEPRECATED
371
+ #
372
+ def send_request request, **options
373
+
374
+ trace ParamNames[ :request, :options ], request, options
375
+
376
+ options.reject! { |k,v| v.empty? if v.respond_to? :empty? }
377
+
378
+ unless (options[:username] && options[:password]) ||
379
+ options[:single_sign_on] ||
380
+ options[:impersonatee] ||
381
+ options[:session_id]
382
+ raise ArgumentError, 'Some form of credentials must be provided.'
383
+ end
384
+
385
+ # Disable session option if session already provided
386
+ options[:session] = false if options[:session_id]
387
+
388
+ options = @options.merge options.reject { |k| [ :config_as_string, :config ].include? k }
389
+
390
+ raise ArgumentError, ':request_as_string option no longer supported' if options[:request_as_string]
391
+
392
+ if (options[:razor_environment] or options[:razor_space]) and options[:razor_alias]
393
+ raise ArgumentError, 'option :razor_alias may not be combined with :razor_environment or :razor_space'
394
+ end
395
+
396
+ warn ':domain option will be ignored as no username was specified' if options[:domain] and not options[:username]
397
+ warn ':password option will be ignored as no username was specified' if options[:password] and not options[:username]
398
+
399
+ if options[:config_as_string]
400
+
401
+ tf = Tempfile.new('clarite_config')
402
+
403
+ begin
404
+
405
+ cc_path = tf.path
406
+
407
+ tf.close
408
+
409
+ File.open(cc_path, 'w') do |f|
410
+
411
+ f.write @config
412
+ f.write "\n"
413
+ end
414
+
415
+ do_send_request_ cc_path, request, options.reject { |k| k == :config_as_string }
416
+ ensure
417
+
418
+ File.unlink(cc_path)
419
+ end
420
+ else
421
+
422
+ do_send_request_ @config, request, options
423
+ end
424
+ end
425
+
426
+ private
427
+ def do_send_request_ config_path, request, options
428
+
429
+ trace ParamNames[ :config_path, :request, :options ], config_path, request, options
430
+
431
+ unless options[:request_as_path] || request.nil?
432
+
433
+ rq_file = Tempfile.new('request_document')
434
+
435
+ begin
436
+
437
+ rq_file.write request
438
+ rq_file.write "\n"
439
+ rq_file.flush
440
+ rq_file.rewind
441
+
442
+ return do_send_request_ config_path, rq_file.path, options.merge({ :request_as_path => true })
443
+ ensure
444
+
445
+ rq_file.close
446
+ rq_file.unlink unless options[:debug]
447
+ end
448
+ end
449
+
450
+ options[:log_to_stderr] = options[:log_to_stderr] || options[:debug]
451
+
452
+ cmd = @executable.form_command config_path, request, **options
453
+
454
+ log :debug0, 'cmd: \'', cmd, '\''
455
+
456
+ t_before = Time.now
457
+
458
+ stdout_str, stderr_str, status = Open3.capture3 cmd
459
+
460
+ t_after = Time.now
461
+
462
+ if severity_logged? :debug1
463
+
464
+ log :debug1, 'stderr: \'', stderr_str.chomp, '\''
465
+ log :debug1, 'stdout: \'', stdout_str.chomp, '\''
466
+ log :debug1, 'status: \'', status, '\''
467
+ end
468
+
469
+ log :benchmark, "execution of RazorRequest(.exe) (in #{self.class}##{__method__}): dt=#{(t_after - t_before) * 1000}ms" if severity_logged?(:benchmark)
470
+
471
+ exit_status = status.exitstatus
472
+
473
+ if 0 != exit_status
474
+
475
+ # Current version of RazorRequest writes its contingent report to
476
+ # stdout - I know! - when it fails (with non-0 exit code), so we
477
+ # must parse that
478
+
479
+ cr_str = options[:log_to_stderr] ? stderr_str : stdout_str
480
+
481
+ cr_lines = cr_str.split(/[\r\n]/)
482
+ contingent_report = cr_lines[-1]
483
+
484
+ # now parse the CR to extract the Razor code
485
+
486
+ rzc = nil
487
+
488
+ cr_lines.each_with_index do |line0, index0|
489
+
490
+ line = line0.chomp
491
+ index = 1 + index0
492
+
493
+ unless rzc
494
+
495
+ rzc = $1 if line =~ /^(RZC\d+)\s+(.*)$/
496
+ end
497
+ end
498
+
499
+ log :debug2, "contingent report lines:\n\t#{cr_lines.join(%Q<\n\t>)}" if options[:debug] && severity_logged?(:debug2)
500
+
501
+ x_class = RequestFailedException
502
+
503
+ if 15 == exit_status || cr_lines.any? { |line| line =~ /RAZOR log[io]n failed/i }
504
+
505
+ x_class = InvalidCredentialsException
506
+ end
507
+
508
+ raise x_class.new "request failed", exit_status, rzc, contingent_report, contingent_report_lines: cr_lines
509
+ else
510
+
511
+ # record stderr
512
+
513
+ contingent_report = stderr_str.split(/[\r\n]/)
514
+
515
+ log :debug2, "contingent report lines:\n\t#{contingent_report.join(%Q<\n\t>)}" if options[:debug] && severity_logged?(:debug2)
516
+ end
517
+
518
+ if stdout_str.split(/[\r\n]/) == ["<ack>queued</ack>"]
519
+ response = ::Nokogiri.XML(stdout_str)
520
+ else
521
+ xml_doc = begin
522
+
523
+ ::Nokogiri.XML(stdout_str) { |config| config.strict }
524
+ rescue ::Nokogiri::XML::SyntaxError => x
525
+
526
+ raise InvalidResponseException.new 'invalid response', cause: x
527
+ end
528
+
529
+ log(:debug4) { "xml_doc: #{xml_doc.to_xml}" }
530
+
531
+ begin
532
+
533
+ response = Response.new xml_doc
534
+ rescue ArgumentError => x
535
+
536
+ raise InvalidResponseException.new 'invalid response', cause: x
537
+ end
538
+
539
+ log :debug3, 'response: ', response
540
+ end
541
+
542
+ log :debug3, 'response: ', response
543
+ response
544
+ end
545
+ public
546
+
547
+ end
548
+
549
+ # ######################################################################## #
550
+ # modules
551
+
552
+ end # module Razor3
553
+ end # module Connectivity
554
+ end # module Razor
555
+ end # module RazorRisk
556
+
557
+ # ############################## end of file ############################# #
558
+
559
+