razorrisk-razor-connectivity 0.14.10

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,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
+