hoodoo 2.5.1 → 2.6.0

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.
@@ -355,5 +355,11 @@ module Hoodoo
355
355
  return endpoint
356
356
  end
357
357
 
358
+ # Alias of #resource, as syntax sugar for those who prefer to think of
359
+ # the return value as an endpoint that is used to contact a resource,
360
+ # rather than a remote abstraction of the resource as an entity.
361
+ #
362
+ alias_method :endpoint, :resource
363
+
358
364
  end
359
365
  end
@@ -456,7 +456,7 @@ module Hoodoo
456
456
  #
457
457
  def handle_exception( exception, communicator )
458
458
  begin
459
- report = "Slow communicator class #{ communicator.class.name } raised exception '#{ exception }': #{ exception.backtrace }"
459
+ report = "Communicator class #{ communicator.class.name } raised exception '#{ exception }': #{ exception.backtrace }"
460
460
  $stderr.puts( report )
461
461
 
462
462
  rescue
@@ -149,5 +149,11 @@ module Hoodoo; module Services
149
149
  return endpoint
150
150
  end
151
151
 
152
+ # Alias of #resource, as syntax sugar for those who prefer to think of
153
+ # the return value as an endpoint that is used to contact a resource,
154
+ # rather than a remote abstraction of the resource as an entity.
155
+ #
156
+ alias_method :endpoint, :resource
157
+
152
158
  end
153
159
  end; end
@@ -74,12 +74,12 @@ module Hoodoo; module Services
74
74
  # intentionally compatible so that pass-through / proxy scenarios from
75
75
  # resource implementation to another resource are assisted:
76
76
  #
77
- # * +"offset"+
78
- # * +"limit"+
79
- # * +"sort"+ (keys from the Hash under attribute #sort_data)
80
- # * +"direction"+ (values from the Hash under #sort_data)
81
- # * +"search"+ (deep-duplcated value of attribute #search_data)
82
- # * +"filter"+ (deep-duplcated value of attribute #filter_data)
77
+ # * +offset+
78
+ # * +limit+
79
+ # * +sort+ (keys from the Hash under attribute #sort_data)
80
+ # * +direction+ (values from the Hash under #sort_data)
81
+ # * +search+ (deep-duplicated value of attribute #search_data)
82
+ # * +filter+ (deep-duplicated value of attribute #filter_data)
83
83
  #
84
84
  # Sort, direction, search and filter data, if not empty, also have
85
85
  # String keys / values. A single sort-direction key-value pair will be
@@ -12,11 +12,11 @@ module Hoodoo
12
12
  # The Hoodoo gem version. If this changes, be sure to re-run
13
13
  # <tt>bundle install</tt> or <tt>bundle update</tt>.
14
14
  #
15
- VERSION = '2.5.1'
15
+ VERSION = '2.6.0'
16
16
 
17
17
  # The Hoodoo gem date. If this changes, be sure to re-run
18
18
  # <tt>bundle install</tt> or <tt>bundle update</tt>.
19
19
  #
20
- DATE = '2018-05-18'
20
+ DATE = '2018-05-23'
21
21
 
22
22
  end
@@ -17,6 +17,11 @@ describe Hoodoo::ActiveRecord::Secure do
17
17
  ActiveRecord::Migration.create_table( :r_spec_model_secure_test_as, &migration )
18
18
  ActiveRecord::Migration.create_table( :r_spec_model_secure_test_bs, &migration )
19
19
  ActiveRecord::Migration.create_table( :r_spec_model_secure_test_cs, &migration )
20
+ ActiveRecord::Migration.create_table( :r_spec_model_secure_test_ds, &migration )
21
+ ActiveRecord::Migration.create_table( :r_spec_model_secure_test_es, &migration )
22
+ ActiveRecord::Migration.create_table( :r_spec_model_secure_test_fs, &migration )
23
+
24
+ # ======================================================================
20
25
 
21
26
  class RSpecModelSecureTestA < ActiveRecord::Base
22
27
  include Hoodoo::ActiveRecord::Secure
@@ -27,6 +32,8 @@ describe Hoodoo::ActiveRecord::Secure do
27
32
  )
28
33
  end
29
34
 
35
+ # ======================================================================
36
+
30
37
  class RSpecModelSecureTestB < ActiveRecord::Base
31
38
  include Hoodoo::ActiveRecord::Secure
32
39
 
@@ -36,6 +43,8 @@ describe Hoodoo::ActiveRecord::Secure do
36
43
  )
37
44
  end
38
45
 
46
+ # ======================================================================
47
+
39
48
  class RSpecModelSecureTestC < ActiveRecord::Base
40
49
  include Hoodoo::ActiveRecord::Secure
41
50
 
@@ -62,9 +71,49 @@ describe Hoodoo::ActiveRecord::Secure do
62
71
  }
63
72
  )
64
73
  end
74
+
75
+ # ======================================================================
76
+
77
+ class RSpecModelSecureTestD < ActiveRecord::Base
78
+ include Hoodoo::ActiveRecord::Secure
79
+
80
+ secure_with(
81
+ :creator => {
82
+ :session_field_name => 'authorised_creators',
83
+ :exemptions => Hoodoo::ActiveRecord::Secure::ENUMERABLE_INCLUDES_STAR
84
+ }
85
+ )
86
+ end
87
+
88
+ class RSpecModelSecureTestE < ActiveRecord::Base
89
+ include Hoodoo::ActiveRecord::Secure
90
+
91
+ secure_with(
92
+ 'distributor' => {
93
+ :session_field_name => :authorised_distributor,
94
+ :exemptions => Hoodoo::ActiveRecord::Secure::OBJECT_EQLS_STAR
95
+ }
96
+ )
97
+ end
98
+
99
+ class RSpecModelSecureTestF < ActiveRecord::Base
100
+ include Hoodoo::ActiveRecord::Secure
101
+
102
+ secure_with(
103
+ :field => {
104
+ :session_field_name => :authorised_suppliers,
105
+ :exemptions => Proc.new { | security_values |
106
+ security_values.is_a?( Enumerable ) &&
107
+ security_values.include?( 'Dolphin' ) rescue false
108
+ }
109
+ }
110
+ )
111
+ end
65
112
  end
66
113
  end
67
114
 
115
+ # ==========================================================================
116
+
68
117
  before :each do
69
118
  # Get a good-enough-for-test interaction which has a context
70
119
  # that contains a Session we can modify.
@@ -202,6 +251,8 @@ describe Hoodoo::ActiveRecord::Secure do
202
251
  it_behaves_like 'a secure model'
203
252
  end
204
253
 
254
+ # ==========================================================================
255
+
205
256
  context 'works with custom Procs' do
206
257
  before :each do
207
258
  @scoped_4 = RSpecModelSecureTestC.new
@@ -265,6 +316,168 @@ describe Hoodoo::ActiveRecord::Secure do
265
316
  end
266
317
  end
267
318
 
319
+ # ==========================================================================
320
+
321
+ # We assume that security_helper_spec.rb takes care of all of the security
322
+ # helper Proc generators, so all that's left to test is the out-of-box Procs
323
+ # defined in constants within 'secure.rb' and make sure a custom Proc works
324
+ # as expected.
325
+ #
326
+ context 'with security exemptions' do
327
+
328
+ # Call the shared examples with the Hash to set in the @session scoping
329
+ # value, then true/false values for whether a scoped find-by-ID query
330
+ # should find the three records created below (true => should find it).
331
+ #
332
+ shared_examples 'a secure model' do | session_scoping, s4_bool, s5_bool, s6_bool |
333
+ before :each do
334
+ @scoped_4 = @class_to_test.new
335
+ @scoped_4.creator = 'C1'
336
+ @scoped_4.distributor = 'D1'
337
+ @scoped_4.field = 'S1'
338
+ @scoped_4.save!
339
+
340
+ @scoped_5 = @class_to_test.new
341
+ @scoped_5.creator = 'C2'
342
+ @scoped_5.distributor = 'D2'
343
+ @scoped_5.field = 'S2'
344
+ @scoped_5.save!
345
+
346
+ @scoped_6 = @class_to_test.new
347
+ @scoped_6.creator = 'C3'
348
+ @scoped_6.distributor = 'D3'
349
+ @scoped_6.field = 'S3'
350
+ @scoped_6.save!
351
+
352
+ @results = [ @scoped4, @scoped5, @scoped6 ]
353
+ end
354
+
355
+ # Convenience method to DRY up a few tests later.
356
+ #
357
+ def expect_none
358
+ found = @class_to_test.secure( @context ).find_by_id( @scoped_4.id )
359
+ expect( found ).to be_nil
360
+
361
+ found = @class_to_test.secure( @context ).find_by_id( @scoped_5.id )
362
+ expect( found ).to be_nil
363
+
364
+ found = @class_to_test.secure( @context ).find_by_id( @scoped_6.id )
365
+ expect( found ).to be_nil
366
+ end
367
+
368
+ it 'finds with exemptions from the class' do
369
+ @session.scoping = session_scoping
370
+
371
+ found = @class_to_test.secure( @context ).find_by_id( @scoped_4.id )
372
+ expect( found ).to eq( s4_bool ? @scoped_4 : nil )
373
+
374
+ found = @class_to_test.secure( @context ).find_by_id( @scoped_5.id )
375
+ expect( found ).to eq( s5_bool ? @scoped_5 : nil )
376
+
377
+ found = @class_to_test.secure( @context ).find_by_id( @scoped_6.id )
378
+ expect( found ).to eq( s6_bool ? @scoped_6 : nil )
379
+ end
380
+
381
+ # Only fully effective if at some point there is a "true, true, true"
382
+ # case which expects to match all. Otherwise, some of the expectations
383
+ # aren't useful (the "expects daft lookup to be nil" cases would be
384
+ # nil anyway, if 'false' indicates no-find-result-expected).
385
+ #
386
+ it 'finds with exemptions with a chain' do
387
+ @session.scoping = session_scoping
388
+
389
+ found = @class_to_test.where( :field => @scoped_4.field ).secure( @context ).find_by_id( @scoped_4.id )
390
+ expect( found ).to eq( s4_bool ? @scoped_4 : nil )
391
+
392
+ found = @class_to_test.where( :field => @scoped_4.field + '!' ).secure( @context ).find_by_id( @scoped_4.id )
393
+ expect( found ).to be_nil
394
+
395
+ found = @class_to_test.where( :field => @scoped_5.field ).secure( @context ).find_by_id( @scoped_5.id )
396
+ expect( found ).to eq( s5_bool ? @scoped_5 : nil )
397
+
398
+ found = @class_to_test.where( :field => @scoped_5.field + '!' ).secure( @context ).find_by_id( @scoped_5.id )
399
+ expect( found ).to be_nil
400
+
401
+ found = @class_to_test.where( :field => @scoped_6.field ).secure( @context ).find_by_id( @scoped_6.id )
402
+ expect( found ).to eq( s6_bool ? @scoped_6 : nil )
403
+
404
+ found = @class_to_test.where( :field => @scoped_6.field + '!' ).secure( @context ).find_by_id( @scoped_6.id )
405
+ expect( found ).to be_nil
406
+ end
407
+
408
+ it 'finds nothing if scope lacks required values' do
409
+ new_session_scoping = {}
410
+
411
+ session_scoping.each do | key, value |
412
+ new_session_scoping[ key ] = if value.is_a?( Array )
413
+ [ 'will not match any records' ]
414
+ else
415
+ 'will not match any records'
416
+ end
417
+ end
418
+
419
+ @session.scoping = new_session_scoping
420
+ expect_none()
421
+ end
422
+
423
+ it 'finds nothing if scope lacks required keys' do
424
+ @session.scoping = { :some_authorised_thing => '*' }
425
+ expect_none()
426
+ end
427
+
428
+ it 'finds nothing if scope is missing' do
429
+ expect_none()
430
+ end
431
+ end
432
+
433
+ # A reminder of RSpecModelSecureTestD rules:
434
+ #
435
+ # :creator => {
436
+ # :session_field_name => 'authorised_creators',
437
+ # :exemptions => Hoodoo::ActiveRecord::Secure::ENUMERABLE_INCLUDES_STAR
438
+ # }
439
+ #
440
+ context 'works with ENUMERABLE_INCLUDES_STAR' do
441
+ before( :all ) { @class_to_test = RSpecModelSecureTestD }
442
+
443
+ it_behaves_like 'a secure model', { :authorised_creators => [ 'C1', '*' ] }, true, true, true
444
+ it_behaves_like 'a secure model', { 'authorised_creators' => [ 'C2' ] }, false, true, false
445
+ end
446
+
447
+ # A reminder of RSpecModelSecureTestE rules:
448
+ #
449
+ # 'distributor' => {
450
+ # :session_field_name => :authorised_distributor,
451
+ # :exemptions => Hoodoo::ActiveRecord::Secure::OBJECT_EQLS_STAR
452
+ # }
453
+ #
454
+ context 'works with OBJECT_EQLS_STAR' do
455
+ before( :all ) { @class_to_test = RSpecModelSecureTestE }
456
+
457
+ it_behaves_like 'a secure model', { :authorised_distributor => '*' }, true, true, true
458
+ it_behaves_like 'a secure model', { 'authorised_distributor' => 'D3' }, false, false, true
459
+ end
460
+
461
+ # A reminder of RSpecModelSecureTestF rules:
462
+ #
463
+ # :field => {
464
+ # :session_field_name => :authorised_suppliers,
465
+ # :exemptions => Proc.new { | security_values |
466
+ # security_values.is_a?( Enumerable ) &&
467
+ # security_values.include?( 'Dolphin' ) rescue false
468
+ # }
469
+ # }
470
+ #
471
+ context 'works with a custom Proc' do
472
+ before( :all ) { @class_to_test = RSpecModelSecureTestF }
473
+
474
+ it_behaves_like 'a secure model', { :authorised_suppliers => [ 'Dolphin' ] }, true, true, true
475
+ it_behaves_like 'a secure model', { 'authorised_suppliers' => [ 'S1', 'S3' ] }, true, false, true
476
+ end
477
+ end
478
+
479
+ # ==========================================================================
480
+
268
481
  # See also presenters/base_spec.rb
269
482
  #
270
483
  context 'rendering' do
@@ -0,0 +1,201 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hoodoo::ActiveRecord::Secure::SecurityHelper do
4
+
5
+ class TestAllMatchersObject
6
+ include Enumerable
7
+
8
+ def eql?( thing ); true; end
9
+ def include?( thing ); true; end
10
+ def match?( thing ); true; end
11
+ end
12
+
13
+ class TestRescueAllMatchersObject
14
+ include Enumerable
15
+
16
+ def eql?( thing ); raise "boo!"; end
17
+ def include?( thing ); raise "boo!"; end
18
+ def match?( thing ); raise "boo!"; end
19
+ end
20
+
21
+ context '::eqls_wildcard' do
22
+ before :each do
23
+ @proc = Hoodoo::ActiveRecord::Secure::SecurityHelper.eqls_wildcard( '!' )
24
+ end
25
+
26
+ it 'matches when it should' do
27
+ expect( @proc.call( '!' ) ).to eql( true )
28
+ expect( @proc.call( TestAllMatchersObject.new ) ).to eql( true )
29
+ end
30
+
31
+ it 'misses when it should' do
32
+ expect( @proc.call( '*' ) ).to eql( false )
33
+ expect( @proc.call( ' !' ) ).to eql( false )
34
+ expect( @proc.call( '! ' ) ).to eql( false )
35
+ expect( @proc.call( "!\n" ) ).to eql( false )
36
+ expect( @proc.call( 42 ) ).to eql( false )
37
+ expect( @proc.call( { :hello => :world } ) ).to eql( false )
38
+ expect( @proc.call( [ 1, 2, 3, 4 ] ) ).to eql( false )
39
+ end
40
+
41
+ it 'rescues' do
42
+ expect( @proc.call( TestRescueAllMatchersObject.new ) ).to eql( false )
43
+ end
44
+ end
45
+
46
+ context '::includes_wildcard' do
47
+ before :each do
48
+ @proc = Hoodoo::ActiveRecord::Secure::SecurityHelper.includes_wildcard( '!' )
49
+ end
50
+
51
+ it 'matches when it should' do
52
+ expect( @proc.call( [ '!' ] ) ).to eql( true )
53
+ expect( @proc.call( [ '!', 1, 2, 3, 4 ] ) ).to eql( true )
54
+ expect( @proc.call( [ 1, 2, '!', 3, 4 ] ) ).to eql( true )
55
+ expect( @proc.call( [ 1, 2, 3, 4, '!' ] ) ).to eql( true )
56
+ expect( @proc.call( TestAllMatchersObject.new ) ).to eql( true )
57
+ end
58
+
59
+ it 'misses when it should' do
60
+ expect( @proc.call( '!' ) ).to eql( false )
61
+ expect( @proc.call( [ '*' ] ) ).to eql( false )
62
+ expect( @proc.call( [ ' !' ] ) ).to eql( false )
63
+ expect( @proc.call( [ '! ' ] ) ).to eql( false )
64
+ expect( @proc.call( [ "!\n" ] ) ).to eql( false )
65
+ expect( @proc.call( 42 ) ).to eql( false )
66
+ expect( @proc.call( { :hello => :world } ) ).to eql( false )
67
+ end
68
+
69
+ it 'rescues' do
70
+ expect( @proc.call( TestRescueAllMatchersObject.new ) ).to eql( false )
71
+ end
72
+ end
73
+
74
+ context '::matches_wildcard' do
75
+ let( :param ) { '^..!.*' }
76
+ let( :proc ) { Hoodoo::ActiveRecord::Secure::SecurityHelper.matches_wildcard( param ) }
77
+
78
+ shared_examples 'a ::matches_wildcard Proc' do
79
+ it 'and matches when it should' do
80
+ expect( proc().call( '12!' ) ).to eql( true )
81
+ expect( proc().call( '12!3' ) ).to eql( true )
82
+
83
+ if ''.respond_to?( :match? )
84
+ expect( proc().call( TestAllMatchersObject.new ) ).to eql( true )
85
+ else
86
+ expect_any_instance_of( Regexp ).to receive( :match ).and_return( true )
87
+ proc().call( TestAllMatchersObject.new )
88
+ end
89
+ end
90
+
91
+ it 'and misses when it should' do
92
+ expect( proc().call( '123!4' ) ).to eql( false )
93
+ expect( proc().call( '1!4' ) ).to eql( false )
94
+ expect( proc().call( 42 ) ).to eql( false )
95
+ expect( proc().call( { :hello => :world } ) ).to eql( false )
96
+ expect( proc().call( [ 1, 2, 3, 4 ] ) ).to eql( false )
97
+ end
98
+
99
+ it 'and rescues' do
100
+ if ''.respond_to?( :match? )
101
+ expect( proc().call( TestRescueAllMatchersObject.new ) ).to eql( false )
102
+ else
103
+ expect_any_instance_of( Regexp ).to receive( :match ).and_raise( RuntimeError )
104
+ proc().call( TestRescueAllMatchersObject.new )
105
+ end
106
+ end
107
+ end
108
+
109
+ # Tests running on Ruby >= 2.4 need String#match? knocking out for a
110
+ # while, for code coverage.
111
+ #
112
+ context 'with slow matcher' do
113
+ before :each do
114
+ @unbound_method = nil
115
+
116
+ if ''.respond_to?( :match? )
117
+ @unbound_method = String.instance_method( :match? )
118
+ String.send( :remove_method, :match? )
119
+ end
120
+ end
121
+
122
+ after :each do
123
+ unless @unbound_method.nil?
124
+ String.send( :define_method, :match?, @unbound_method )
125
+ end
126
+ end
127
+
128
+ context 'constructed with a String' do
129
+ it_behaves_like 'a ::matches_wildcard Proc'
130
+ end
131
+
132
+ context 'constructed with a Regexp' do
133
+ let( :param ) { /^..!.*/ }
134
+ it_behaves_like 'a ::matches_wildcard Proc'
135
+ end
136
+ end
137
+
138
+ # Tests running on Ruby < 2.4 can't do the fast match tests.
139
+ #
140
+ if ''.respond_to?( :match? )
141
+ context 'with fast matcher' do
142
+ context 'constructed with a String' do
143
+ it_behaves_like 'a ::matches_wildcard Proc'
144
+ end
145
+
146
+ context 'constructed with a Regexp' do
147
+ let( :param ) { /^..!.*/ }
148
+ it_behaves_like 'a ::matches_wildcard Proc'
149
+ end
150
+ end
151
+ end
152
+ end
153
+
154
+ context '::matches_wildcard_enumerable' do
155
+ let( :proc ) { Hoodoo::ActiveRecord::Secure::SecurityHelper.matches_wildcard_enumerable( param ) }
156
+ let( :param ) { '^..!.*' }
157
+
158
+ shared_examples 'a ::matches_wildcard Proc' do
159
+ it 'and matches when it should' do
160
+ expect( proc().call( [ '12!34', '1', 2, :three, 4 ] ) ).to eql( true )
161
+ expect( proc().call( [ '1', 2, :three, '12!34', 4 ] ) ).to eql( true )
162
+ expect( proc().call( [ '1', 2, :three, 4, '12!34' ] ) ).to eql( true )
163
+ expect( proc().call( [ '12!' ] ) ).to eql( true )
164
+
165
+ if ''.respond_to?( :match? )
166
+ expect( proc().call( [ TestAllMatchersObject.new ] ) ).to eql( true )
167
+ else
168
+ expect_any_instance_of( Regexp ).to receive( :match ).and_return( true )
169
+ proc().call( [ TestAllMatchersObject.new ] )
170
+ end
171
+ end
172
+
173
+ it 'and misses when it should' do
174
+ expect( proc().call( [ '123!34' ] ) ).to eql( false )
175
+ expect( proc().call( [ '1!4' ] ) ).to eql( false )
176
+ expect( proc().call( { :hello => :world } ) ).to eql( false )
177
+ expect( proc().call( [ 1, 2, 3, 4 ] ) ).to eql( false )
178
+ end
179
+
180
+ it 'and rescues' do
181
+ expect( proc().call( 42 ) ).to eql( false )
182
+
183
+ if ''.respond_to?( :match? )
184
+ expect( proc().call( [ TestRescueAllMatchersObject.new ] ) ).to eql( false )
185
+ else
186
+ expect_any_instance_of( Regexp ).to receive( :match ).and_raise( RuntimeError )
187
+ proc().call( [ TestRescueAllMatchersObject.new ] )
188
+ end
189
+ end
190
+ end
191
+
192
+ context 'constructed with a String' do
193
+ it_behaves_like 'a ::matches_wildcard Proc'
194
+ end
195
+
196
+ context 'constructed with a Regexp' do
197
+ let( :param ) { /^..!.*/ }
198
+ it_behaves_like 'a ::matches_wildcard Proc'
199
+ end
200
+ end
201
+ end