hoodoo 2.5.1 → 2.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA256:
3
- metadata.gz: bcd6275929dde876479da0087f2eef17e4d9fd3c8606182daeff2fd4c02c35c9
4
- data.tar.gz: 3763e2d462802bbdae8240864e7bf73b83eb5458675da127af70edba8013dc1b
2
+ SHA1:
3
+ metadata.gz: cbae2491898cb113ffb0d3b9580e860e050d0fd5
4
+ data.tar.gz: d7dd9c51af9a0015572d9cb46d5a0fb63ee679c4
5
5
  SHA512:
6
- metadata.gz: 6492416bb7afa5d8e7d9a23169293f055c73a6b9e8c63362f65b08ce7f49bd219874be90480ea7156c02373725e8088b19f9dfdda3a9ba2329b3037b7bc8b958
7
- data.tar.gz: a8142c1c0973b77b1101af5f757b088b81a11bac48cce8597691288dcc288c8a786efd1ea6dd16820c44b9c222fb8df351cb739e6a2a6ea3e248f09b3943bacb
6
+ metadata.gz: 1e2bf59ec7c80eb94e45a3cb549f18f783b4fce0d5b263c4da9b92b7bf01c69b32f8a0980f5ce25f7a7ebc4bff5f0d60d63e2b51dea93784491382abc58f0571
7
+ data.tar.gz: 0a55cfb887c0fbf48594081bc2cd44e51b48c3e46dbc22018937acfc6645caca60e1981fcc3f49dea5883b8a925ae37dc1e9c2972e88472ae7436d7957651795
@@ -393,8 +393,9 @@ module Hoodoo
393
393
  )
394
394
 
395
395
  unless model == Hoodoo::ActiveRecord::Base
396
- model.send( :include, Hoodoo::ActiveRecord::UUID )
396
+ model.send( :include, Hoodoo::ActiveRecord::UUID )
397
397
  model.send( :include, Hoodoo::ActiveRecord::Finder )
398
+
398
399
  instantiate( model )
399
400
  end
400
401
 
@@ -9,6 +9,8 @@
9
9
  # 25-Nov-2014 (ADH): Created.
10
10
  ########################################################################
11
11
 
12
+ require 'hoodoo/active/active_record/security_helper'
13
+
12
14
  module Hoodoo
13
15
  module ActiveRecord
14
16
 
@@ -58,6 +60,16 @@ module Hoodoo
58
60
  model.extend( ClassMethods )
59
61
  end
60
62
 
63
+ # Convenience constant defining an equals-single-security-value wildcard
64
+ # security exemption using the String '*'.
65
+ #
66
+ OBJECT_EQLS_STAR = Hoodoo::ActiveRecord::Secure::SecurityHelper::eqls_wildcard( '*' )
67
+
68
+ # Convenience constant defining an included-in-enumerable-security-value
69
+ # wildcard security excemption using the String '*'.
70
+ #
71
+ ENUMERABLE_INCLUDES_STAR = Hoodoo::ActiveRecord::Secure::SecurityHelper::includes_wildcard( '*' )
72
+
61
73
  # Collection of class methods that get defined on an including class via
62
74
  # Hoodoo::ActiveRecord::Secure::included.
63
75
  #
@@ -69,7 +81,9 @@ module Hoodoo
69
81
  # alternative argument generator is given in the longhand form's
70
82
  # value Hash's +:using+ key.
71
83
  #
72
- DEFAULT_SECURE_PROC = Proc.new { | model_class, database_column_name, session_field_value | [ { database_column_name => session_field_value } ] }
84
+ DEFAULT_SECURE_PROC = Proc.new do | model_class, database_column_name, session_field_value |
85
+ [ { database_column_name => session_field_value } ]
86
+ end
73
87
 
74
88
  # The core of out-of-the-box Hoodoo data access security layer.
75
89
  #
@@ -255,6 +269,9 @@ module Hoodoo
255
269
  # +using+:: See the _Advanced_ _query_ _conditions_
256
270
  # section later for details.
257
271
  #
272
+ # +exemptions+:: See the _Security_ _exemptions_ section later
273
+ # for details.
274
+ #
258
275
  # To help clarify the above, the following two calls to #secure_with
259
276
  # have exactly the same effect.
260
277
  #
@@ -320,7 +337,7 @@ module Hoodoo
320
337
  #
321
338
  # This leads to SQL along the following lines:
322
339
  #
323
- # AND ("model_table"."creating_caller_uuid" = '[val]')
340
+ # AND ("model_table"."creating_caller_uuid" IN ('[val]'))
324
341
  #
325
342
  # ...where <tt>val</tt> is from the Session +authorised_caller_uuids+
326
343
  # data in the +scoping+ section (so this might be an SQL +IN+ rather
@@ -352,7 +369,7 @@ module Hoodoo
352
369
  #
353
370
  # ...yields something like:
354
371
  #
355
- # AND ( "model_table"."creating_caller_uuid" = '[val]' OR "model_table"."other_column_name" = '[val]' )
372
+ # AND ( "model_table"."creating_caller_uuid" IN ('[val]') OR "model_table"."other_column_name" IN ('[val]') )
356
373
  #
357
374
  # A Proc specified with +:using+ is called with:
358
375
  #
@@ -369,6 +386,92 @@ module Hoodoo
369
386
  # +where+ via <tt>where( *returned_values )</tt> as part of the wider
370
387
  # query chain.
371
388
  #
389
+ # == Security exemptions
390
+ #
391
+ # Sometimes you might want a security bypass mechanism for things like
392
+ # a Superuser style caller that can "see everything". It's more secure,
393
+ # where possible and scalable, to simply have the session data match
394
+ # every known value of some particular secured-with quantity, but this
395
+ # might get unwieldy. "WHERE IN" queries with hundreds or thousands of
396
+ # listed items can cause problems!
397
+ #
398
+ # Noting that with any security exemption there is elevated risk, you
399
+ # can use the +:exemptions+ key to provide a Proc which is passed the
400
+ # secure value(s) under consideration (the data taken directly from
401
+ # the session scoping section) and evaluates to +true+ if the value(s)
402
+ # indicate that a security exemption applies, else evaluates "falsey"
403
+ # for normal behaviour. We say "value(s)" here as a single key used to
404
+ # read from the scoping section of a session may yield either a simple
405
+ # value such as a String, or an Enumerable object such as an array of
406
+ # many Strings.
407
+ #
408
+ # If the Proc evaluates to +true+, the result is no modification to the
409
+ # secure scope chain being constructed for the secured ActiveRecord
410
+ # query the caller will eventually run. Helper methods which construct
411
+ # common use case Procs are present in
412
+ # Hoodoo::ActiveRecord::Secure::SecurityHelper and there are
413
+ # convenience constants defined in Hoodoo::ActiveRecord::Secure, such
414
+ # as Hoodoo::ActiveRecord::Secure::ENUMERABLE_INCLUDES_STAR.
415
+ #
416
+ # Taking an earlier example:
417
+ #
418
+ # secure_with( {
419
+ # :creating_caller_uuid => :authorised_caller_uuids
420
+ # } )
421
+ #
422
+ # # ...has this minimal longhand equivalent...
423
+ #
424
+ # secure_with( {
425
+ # :creating_caller_uuid => {
426
+ # :session_field_name => :authorised_caller_uuids
427
+ # }
428
+ # } )
429
+ #
430
+ # ...which leads to SQL along the following lines:
431
+ #
432
+ # AND ("model_table"."creating_caller_uuid" IN ('[val]'))
433
+ #
434
+ # ...then suppose we wanted to allow a session scoping value of '*'
435
+ # bypass security ("see everything"). We could use the
436
+ # Enumerable-includes-star matcher Proc
437
+ # Hoodoo::ActiveRecord::Secure::ENUMERABLE_INCLUDES_STAR here. At the
438
+ # time of writing, it is defined as the following Proc:
439
+ #
440
+ # Proc.new do | security_values |
441
+ # security_values.is_a?( Enumerable ) &&
442
+ # security_values.include?( '*' ) rescue false
443
+ # end
444
+ #
445
+ # This is activated through the +:exemptions+ key:
446
+ #
447
+ # secure_with( {
448
+ # :creating_caller_uuid => {
449
+ # :session_field_name => :authorised_caller_uuids,
450
+ # :exemptions => Hoodoo::ActiveRecord::Secure::ENUMERABLE_INCLUDES_STAR
451
+ # }
452
+ # } )
453
+ #
454
+ # If the looked up value of the +authorised_caller_uuids+ attribute
455
+ # in the prevailing Session scoping section data was ["1234"], then the
456
+ # SQL query additions would occur as above:
457
+ #
458
+ # AND ("model_table"."creating_caller_uuid" IN ('1234'))
459
+ #
460
+ # ...but if there is a value of "*", the security layer will ignore the
461
+ # normal restrictions, resulting in no SQL additions whatsoever.
462
+ #
463
+ # Since a Proc is used to compare the data found in the session against
464
+ # some wildcard, things like checking an array of values for some magic
465
+ # bypass characters / key, using regular expression matching, or other
466
+ # more heavyweight options are all possible. Remember, though, that all
467
+ # of this comes at a risk, since the mechanism is bypassing the normal
468
+ # scope chain security. If used improperly or somehow compromised, it
469
+ # will allow data to be read by an API caller that should not have been
470
+ # permitted to access it.
471
+ #
472
+ # See module Hoodoo::ActiveRecord::Secure::SecurityHelper for methods
473
+ # to help with exemption Proc construction.
474
+ #
372
475
  def secure( context )
373
476
  prevailing_scope = all() # "Model.all" -> returns anonymous scope
374
477
  extra_scope_map = secured_with()
@@ -377,25 +480,33 @@ module Hoodoo
377
480
  return none() if context.session.nil? || context.session.scoping.nil?
378
481
 
379
482
  extra_scope_map.each do | model_field_name, key_or_options |
380
- params_proc = DEFAULT_SECURE_PROC
483
+ exemption_proc = nil
484
+ params_proc = DEFAULT_SECURE_PROC
381
485
 
382
486
  if key_or_options.is_a?( Hash )
383
487
  session_scoping_key = key_or_options[ :session_field_name ]
488
+ exemption_proc = key_or_options[ :exemptions ]
384
489
  params_proc = key_or_options[ :using ] if key_or_options.has_key?( :using )
385
490
  else
386
491
  session_scoping_key = key_or_options
387
492
  end
388
493
 
389
494
  if context.session.scoping.respond_to?( session_scoping_key )
390
- args = params_proc.call(
391
- self,
392
- model_field_name,
393
- context.session.scoping.send( session_scoping_key )
394
- )
395
- prevailing_scope = prevailing_scope.where( *args )
495
+ security_value = context.session.scoping.send( session_scoping_key )
496
+
497
+ if exemption_proc.nil? || exemption_proc.call( security_value ) != true
498
+ args = params_proc.call(
499
+ self,
500
+ model_field_name,
501
+ security_value
502
+ )
503
+ prevailing_scope = prevailing_scope.where( *args )
504
+ end
505
+
396
506
  else
397
507
  prevailing_scope = none()
398
508
  break
509
+
399
510
  end
400
511
  end
401
512
  end
@@ -0,0 +1,143 @@
1
+ ########################################################################
2
+ # File:: security_helper.rb
3
+ # (C):: Loyalty New Zealand 2018
4
+ #
5
+ # Purpose:: Supplementary helper class included by "finder.rb". See
6
+ # Hoodoo::ActiveRecord::Secure, especially
7
+ # Hoodoo::ActiveRecord::Secure::ClassMethods#secure_with and
8
+ # its options Hash, for details.
9
+ # ----------------------------------------------------------------------
10
+ # 05-Apr-2018 (ADH): Created.
11
+ ########################################################################
12
+
13
+ module Hoodoo
14
+ module ActiveRecord
15
+ module Secure
16
+
17
+ # Help build security exemption Procs to pass into
18
+ # Hoodoo::ActiveRecord::Secure::ClassMethods#secure_with via its options
19
+ # Hash. The following extends an example given in the documentation (at
20
+ # the time of writing here) for the underlying implementation method
21
+ # Hoodoo::ActiveRecord::Secure::ClassMethods#secure:
22
+ #
23
+ # class Audit < ActiveRecord::Base
24
+ # include Hoodoo::ActiveRecord::Secure
25
+ #
26
+ # secure_with(
27
+ # {
28
+ # :creating_caller_uuid => :authorised_caller_uuids
29
+ # },
30
+ # {
31
+ # :exemptions => Hoodoo::ActiveRecord::Secure::SecurityHelper::includes_wildcard( '*' )
32
+ # }
33
+ # )
34
+ # end
35
+ #
36
+ # Note that the Hoodoo::ActiveRecord::Secure module includes some belper
37
+ # constants to aid brevity for common cases such as the single value
38
+ # <tt>#eql?</tt> or enumerable <tt>#include?</tt> matchers checking for
39
+ # a '*' as an indiscriminate wildcard - see for example
40
+ # Hoodoo::ActiveRecord::Secure::ENUMERABLE_INCLUDES_STAR.
41
+ #
42
+ class SecurityHelper
43
+
44
+ # Internally used by ::matches_wildcard for Ruby 2.4.0+ performance.
45
+ #
46
+ RUBY_FAST_WILDCARD_PROC_CONTENTS = %q{
47
+ security_value.match?( wildcard_regexp ) rescue false
48
+ }
49
+
50
+ # Internally used by ::matches_wildcard for Ruby < 2.4 compatibility.
51
+ #
52
+ RUBY_SLOW_WILDCARD_PROC_CONTENTS = %q{
53
+ wildcard_regexp.match( security_value ) != nil rescue false
54
+ }
55
+
56
+ # Match a given wildcard, typically a String, to a single value
57
+ # via <tt>#eql?</tt>.
58
+ #
59
+ # +wildcard_value+:: Wildcard value to match, e.g. <tt>'*'</tt>.
60
+ #
61
+ # Returns a Proc suitable for passing to the +:exemptions+ option for
62
+ # Hoodoo::ActiveRecord::Secure::ClassMethods#secure_with.
63
+ #
64
+ def self.eqls_wildcard( wildcard_value )
65
+ Proc.new do | security_value |
66
+ security_value.eql?( wildcard_value ) rescue false
67
+ end
68
+ end
69
+
70
+ # Match a given wildcard, typically a String, inside an Enumerable
71
+ # subclass via <tt>#include?</tt>.
72
+ #
73
+ # +wildcard_value+:: Wildcard value to match, e.g. <tt>'*'</tt>.
74
+ #
75
+ # Returns a Proc suitable for passing to the +:exemptions+ option for
76
+ # Hoodoo::ActiveRecord::Secure::ClassMethods#secure_with.
77
+ #
78
+ def self.includes_wildcard( wildcard_value )
79
+ Proc.new do | security_values |
80
+ security_values.is_a?( Enumerable ) &&
81
+ security_values.include?( wildcard_value ) rescue false
82
+ end
83
+ end
84
+
85
+ # Match a given wildcard Regexp to a value via <tt>#match?</tt>.
86
+ #
87
+ # +wildcard_value+:: Wildcard Regexp to use, e.g. <tt>/.*/</tt>.
88
+ # Strings are coerced to Regexps without any
89
+ # escaping but doing so reduces performance.
90
+ #
91
+ # Returns a Proc suitable for passing to the +:exemptions+ option for
92
+ # Hoodoo::ActiveRecord::Secure::ClassMethods#secure_with.
93
+ #
94
+ def self.matches_wildcard( wildcard_regexp )
95
+ wildcard_regexp = Regexp.new( wildcard_regexp ) unless wildcard_regexp.is_a?( Regexp )
96
+
97
+ # Use security_value's #match? (if present) to ensure that we have
98
+ # an expected "matchable" type. This is only available in Ruby 2.4
99
+ # or later, so a patch is performed below for earlier Rubies.
100
+ #
101
+ Proc.new do | security_value |
102
+
103
+ # Ruby 2.4.0 and later introduce the Regexp#match? family, which
104
+ # is the fastest way to determine a simple does-or-does-not match
105
+ # condition. Ruby 2.3.x and earlier need different, slower code.
106
+ #
107
+ if ''.respond_to?( :match? )
108
+ eval( RUBY_FAST_WILDCARD_PROC_CONTENTS )
109
+ else
110
+ eval( RUBY_SLOW_WILDCARD_PROC_CONTENTS )
111
+ end
112
+ end
113
+ end
114
+
115
+ # Match a given wildcard Regexp to any value in an enumerable
116
+ # object via iteration and <tt>#match?</tt>. Exists with +true+
117
+ # as soon as any match is made.
118
+ #
119
+ # +wildcard_value+:: Wildcard Regexp to use, e.g. <tt>/.*/</tt>.
120
+ # Strings are coerced to Regexps without any
121
+ # escaping but doing so reduces performance.
122
+ #
123
+ # Returns a Proc suitable for passing to the +:exemptions+ option for
124
+ # Hoodoo::ActiveRecord::Secure::ClassMethods#secure_with.
125
+ #
126
+ def self.matches_wildcard_enumerable( wildcard_regexp )
127
+ match_proc = self.matches_wildcard( wildcard_regexp )
128
+
129
+ Proc.new do | security_values |
130
+ begin
131
+ security_values.any? do | security_value |
132
+ match_proc.call( security_value )
133
+ end
134
+ rescue
135
+ false
136
+ end
137
+ end
138
+ end
139
+
140
+ end
141
+ end
142
+ end
143
+ end
@@ -41,12 +41,17 @@ module Hoodoo
41
41
  # # ...
42
42
  # end
43
43
  #
44
+ # Depends upon and auto-includes Hoodoo::ActiveRecord::Creator
45
+ # and Hoodoo::ActiveRecord::ErrorMapping.
46
+ #
44
47
  # +model+:: The ActiveRecord::Base descendant that is including
45
48
  # this module.
46
49
  #
47
50
  def self.included( model )
48
51
  unless model == Hoodoo::ActiveRecord::Base
52
+ model.send( :include, Hoodoo::ActiveRecord::Creator )
49
53
  model.send( :include, Hoodoo::ActiveRecord::ErrorMapping )
54
+
50
55
  instantiate( model )
51
56
  end
52
57
 
@@ -75,18 +80,93 @@ module Hoodoo
75
80
  end
76
81
  end
77
82
 
78
- # Instance equivalent of
79
- # Hoodoo::ActiveRecord::Writer::ClassMethods.persist_in - see that for
80
- # details. The class method just calls here, having constructed an
81
- # instance based on the attributes it was given. If you have already
82
- # built an instance yourself, just call this instance method equivalent
83
- # instead.
84
- #
85
- # As an instance-based method, the return value and error handling
86
- # semantics differ from the class-based counterpart. Instead of
87
- # checking "persisted?", check the return value of +persist_in+. This
88
- # means you can also use +persist_in+ to save a previously persisted,
89
- # but now updated record, should you so wish.
83
+ # == Overview
84
+ #
85
+ # Service authors _SHOULD_ use this method when persisting data with
86
+ # ActiveRecord if there is a risk of duplication constraint violation
87
+ # of any kind. This will include a violation on the UUID of a resource
88
+ # if you support external setting of this value via the body of a
89
+ # +create+ call containing the +id+ field, injected by Hoodoo as the
90
+ # result of an authorised use of the <tt>X-Resource-UUID</tt> HTTP
91
+ # header.
92
+ #
93
+ # You can use this method for both persisting new records or
94
+ # persisting updates, in the same way as ActiveRecord's +save+ is
95
+ # used for either.
96
+ #
97
+ #
98
+ # == Concurrency
99
+ #
100
+ # Services often run in highly concurrent environments and uniqueness
101
+ # constraint validations with ActiveRecord cannot protect against
102
+ # race conditions in such cases. Those work at the application level;
103
+ # the check to see if a record exists with a duplicate value in some
104
+ # given column is a separate operation from that which stores the
105
+ # record subsequently. As per the Rails Guides entry on the uniqueness
106
+ # validation at the time of writing:
107
+ #
108
+ # http://guides.rubyonrails.org/active_record_validations.html#uniqueness
109
+ #
110
+ # <i>"It does not create a uniqueness constraint in the database, so
111
+ # it may happen that two different database connections create two
112
+ # records with the same value for a column that you intend to be
113
+ # unique. To avoid that, you must create a unique index on both
114
+ # columns in your database."</i>
115
+ #
116
+ # You *MUST* always use a uniqueness constraint at the database level
117
+ # and *MAY* additionally use ActiveRecord validations for a higher
118
+ # level warning in all but race condition edge cases. If you then use
119
+ # this +persist_in+ method to store records, all duplication cases
120
+ # will be handled elegantly and reported as a
121
+ # <tt>generic.invalid_duplication</tt> error. In the event that a
122
+ # caller has used the <tt>X-Deja-Vu</tt> HTTP header, Hoodoo will take
123
+ # such an error and transform it into a non-error 204 HTTP response;
124
+ # so by using +persist_in+, you also ensure that your service
125
+ # participates successfully in this process without any additional
126
+ # coding effort. You get safe concurrency and protection against the
127
+ # inherent lack of idempotency in HTTP +POST+ operations via any
128
+ # must-be-unique fields (within your defined scope) automatically.
129
+ #
130
+ # Using this method for data storage instead of plain ActiveRecord
131
+ # +save+ or <tt>save!</tt> will also help your code auto-inherit any
132
+ # additional future write-related enhancements in Hoodoo should they
133
+ # arise, without necessarily needing service code changes.
134
+ #
135
+ #
136
+ # == Parameters
137
+ #
138
+ # +context+:: Hoodoo::Services::Context instance describing a call
139
+ # context. This is typically a value passed to one of
140
+ # the Hoodoo::Services::Implementation instance methods
141
+ # that a resource subclass implements.
142
+ #
143
+ # Returns a Symbol of +:success+ or +:failure+ indicating the outcome
144
+ # of the same attempt. In the event of failure, the model will be
145
+ # invalid and not persisted; you can read errors immediately and should
146
+ # avoid unnecessarily re-running validations by calling +valid?+ or
147
+ # +validate+ on the instance.
148
+ #
149
+ #
150
+ # == Example
151
+ #
152
+ # class Unique < ActiveRecord::Base
153
+ # include Hoodoo::ActiveRecord::Writer
154
+ # validates :unique_code, :presence => true, :uniqueness => true
155
+ # end
156
+ #
157
+ # The migration to create the table for the Unique model _MUST_ have a
158
+ # uniqueness constraint on the +unique_code+ field, e.g.:
159
+ #
160
+ # def change
161
+ # add_column :uniques, :unique_code, :null => false
162
+ # add_index :uniques, [ :unique_code ], :unique => true
163
+ # end
164
+ #
165
+ # Then, inside the implementation class which uses the above model,
166
+ # where you have (say) written private methods +mapping_of+ which
167
+ # maps +context.request.body+ to an attributes Hash for persistence
168
+ # and +rendering_of+ which uses Hoodoo::Presenters::Base.render_in to
169
+ # properly render a representation of your resource, you would write:
90
170
  #
91
171
  # def create( context )
92
172
  # attributes = mapping_of( context.request.body )
@@ -109,18 +189,49 @@ module Hoodoo
109
189
  # context.response.set_resource( rendering_of( context, model_instance ) )
110
190
  # end
111
191
  #
112
- # Parameters:
113
192
  #
114
- # +context+:: Hoodoo::Services::Context instance describing a call
115
- # context. This is typically a value passed to one of
116
- # the Hoodoo::Services::Implementation instance methods
117
- # that a resource subclass implements.
193
+ # == See also
118
194
  #
119
- # Returns a Symbol of +:success+ or +:failure+ indicating the outcome
120
- # of the same attempt. In the event of failure, the model will be
121
- # invalid and not persisted; you can read errors immediately and should
122
- # avoid unnecessarily re-running validations by calling +valid?+ or
123
- # +validate+ on the instance.
195
+ # There is a class method equivalent which combines creating a new record
196
+ # and persisting it in a single call. If you prefer that code style, see
197
+ # Hoodoo::ActiveRecord::Writer::ClassMethods.persist_in. In such cases,
198
+ # it could look quite odd to mix the class method and instance method
199
+ # variants for new records or existing record updates; as syntax sugar,
200
+ # an alias of the #persist_in instance method is available under the name
201
+ # #update_in, so that you can use the class method for creation and the
202
+ # aliased instance method for updates.
203
+ #
204
+ #
205
+ # == Nested transaction note
206
+ #
207
+ # Ordinarily an exception in a nested transaction does not roll back.
208
+ # ActiveRecord wraps all saves in a transaction "out of the box", so
209
+ # the following construct could have unexpected results...
210
+ #
211
+ # Model.transaction do
212
+ # instance.persist_in( context )
213
+ # end
214
+ #
215
+ # ...if <tt>instance.valid?</tt> runs any SQL queries - which is very
216
+ # likely. PostgreSQL, for example, would then raise an exception; the
217
+ # inner transaction failed, leaving the outer one in an aborted state:
218
+ #
219
+ # PG::InFailedSqlTransaction: ERROR: current transaction is
220
+ # aborted, commands ignored until end of transaction block
221
+ #
222
+ # ActiveRecord provides us with a way to define a transaction that
223
+ # does roll back via the <tt>requires_new: true</tt> option. Hoodoo
224
+ # thus protects callers from the above artefacts by ensuring that all
225
+ # saves are wrapped in an outer transaction that causes rollback in
226
+ # any parents. This sidesteps the unexpected behaviour, but service
227
+ # authors might sometimes need to be aware of this if using complex
228
+ # transaction behaviour along with <tt>persist_in</tt>.
229
+ #
230
+ # In pseudocode, the internal implementation is:
231
+ #
232
+ # self.transaction( :requires_new => true ) do
233
+ # self.save
234
+ # end
124
235
  #
125
236
  def persist_in( context )
126
237
 
@@ -171,77 +282,28 @@ module Hoodoo
171
282
  return errors_occurred.nil? ? :success : :failure
172
283
  end
173
284
 
285
+ # Alias of #persist_in. Although that can be used for new records or
286
+ # updates, it's nice to have the syntax sugar of an "update in context"
287
+ # method to sit alongside things like #persist_in and
288
+ # Hoodoo::ActiveRecord::Creator::ClassMethods::new_in.
289
+ #
290
+ alias_method :update_in, :persist_in
291
+
174
292
  # Collection of class methods that get defined on an including class via
175
293
  # Hoodoo::ActiveRecord::Writer::included.
176
294
  #
177
295
  module ClassMethods
178
296
 
179
- # == Overview
180
- #
181
- # Service authors _SHOULD_ use this method when persisting data with
182
- # ActiveRecord if there is a risk of duplication constraint violation
183
- # of any kind. This will include a violation on the UUID of a resource
184
- # if you support external setting of this value via the body of a
185
- # +create+ call containing the +id+ field, injected by Hoodoo as the
186
- # result of an authorised use of the <tt>X-Resource-UUID</tt> HTTP
187
- # header.
188
- #
189
- # Services often run in highly concurrent environments and uniqueness
190
- # constraint validations with ActiveRecord cannot protect against
191
- # race conditions in such cases. IT works at the application level;
192
- # the check to see if a record exists with a duplicate value in some
193
- # given column is a separate operation from that which stores the
194
- # record subsequently. As per the Rails Guides entry on the uniqueness
195
- # validation at the time of writing:
196
- #
197
- # http://guides.rubyonrails.org/active_record_validations.html#uniqueness
198
- #
199
- # <i>"It does not create a uniqueness constraint in the database, so
200
- # it may happen that two different database connections create two
201
- # records with the same value for a column that you intend to be
202
- # unique. To avoid that, you must create a unique index on both
203
- # columns in your database."</i>
204
- #
205
- # You *MUST* always use a uniqueness constraint at the database level
206
- # and *MAY* additionally use ActiveRecord validations for a higher
207
- # level warning in all but race condition edge cases. If you then use
208
- # this +persist_in+ method to store records, all duplication cases
209
- # will be handled elegantly and reported as a
210
- # <tt>generic.invalid_duplication</tt> error. In the event that a
211
- # caller has used the <tt>X-Deja-Vu</tt> HTTP header, Hoodoo will take
212
- # such an error and transform it into a non-error 204 HTTP response;
213
- # so by using +persist_in+, you also ensure that your service
214
- # participates successfully in this process without any additional
215
- # coding effort. You get safe concurrency and protection against the
216
- # inherent lack of idempotency in HTTP +POST+ operations via any
217
- # must-be-unique fields (within your defined scope) automatically.
297
+ # A class-based equivalent of the
298
+ # Hoodoo::ActiveRecord::Writer#persist_in method which creates a
299
+ # record using Hoodoo::ActiveRecord::Creator::ClassMethods::new_in,
300
+ # then calls Hoodoo::ActiveRecord::Writer#persist_in to persist the
301
+ # data; see that for full details.
218
302
  #
219
- # Using this method for data storage instead of plain ActiveRecord
220
- # +save+ or <tt>save!</tt> will also help your code auto-inherit any
221
- # additional future write-related enhancements in Hoodoo should they
222
- # arise, without necessarily needing service code changes.
223
- #
224
- #
225
- # == Example
226
- #
227
- # class Unique < ActiveRecord::Base
228
- # include Hoodoo::ActiveRecord::Writer
229
- # validates :unique_code, :presence => true, :uniqueness => true
230
- # end
231
- #
232
- # The migration to create the table for the Unique model _MUST_ have a
233
- # uniqueness constraint on the +unique_code+ field, e.g.:
234
- #
235
- # def change
236
- # add_column :uniques, :unique_code, :null => false
237
- # add_index :uniques, [ :unique_code ], :unique => true
238
- # end
239
- #
240
- # Then, inside the implementation class which uses the above model,
241
- # where you have (say) written private methods +mapping_of+ which
242
- # maps +context.request.body+ to an attributes Hash for persistence
243
- # and +rendering_of+ which uses Hoodoo::Presenters::Base.render_in to
244
- # properly render a representation of your resource, you would write:
303
+ # As a class-based method, the return value and error handling
304
+ # semantics differ from the instance-based counterpart. Instead of
305
+ # checking the return value of +persist_in+ for success or failure,
306
+ # use ActiveRecord's "persisted?":
245
307
  #
246
308
  # def create( context )
247
309
  # attributes = mapping_of( context.request.body )
@@ -276,40 +338,8 @@ module Hoodoo
276
338
  # See also the Hoodoo::ActiveRecord::Writer#persist_in instance method
277
339
  # equivalent of this class method.
278
340
  #
279
- #
280
- # == Nested transaction note
281
- #
282
- # Ordinarily an exception in a nested transaction does not roll back.
283
- # ActiveRecord wraps all saves in a transaction "out of the box", so
284
- # the following construct could have unexpected results...
285
- #
286
- # Model.transaction do
287
- # instance.persist_in( context )
288
- # end
289
- #
290
- # ...if <tt>instance.valid?</tt> runs any SQL queries - which is very
291
- # likely. PostgreSQL, for example, would then raise an exception; the
292
- # inner transaction failed, leaving the outer one in an aborted state:
293
- #
294
- # PG::InFailedSqlTransaction: ERROR: current transaction is
295
- # aborted, commands ignored until end of transaction block
296
- #
297
- # ActiveRecord provides us with a way to define a transaction that
298
- # does roll back via the <tt>requires_new: true</tt> option. Hoodoo
299
- # thus protects callers from the above artefacts by ensuring that all
300
- # saves are wrapped in an outer transaction that causes rollback in
301
- # any parents. This sidesteps the unexpected behaviour, but service
302
- # authors might sometimes need to be aware of this if using complex
303
- # transaction behaviour along with <tt>persist_in</tt>.
304
- #
305
- # In pseudocode, the internal implementation is:
306
- #
307
- # self.transaction( :requires_new => true ) do
308
- # self.save
309
- # end
310
- #
311
341
  def persist_in( context, attributes )
312
- instance = self.new( attributes )
342
+ instance = self.new_in( context, attributes )
313
343
  instance.persist_in( context )
314
344
 
315
345
  return instance