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.
- checksums.yaml +5 -5
- data/lib/hoodoo/active/active_record/manually_dated.rb +2 -1
- data/lib/hoodoo/active/active_record/secure.rb +121 -10
- data/lib/hoodoo/active/active_record/security_helper.rb +143 -0
- data/lib/hoodoo/active/active_record/writer.rb +150 -120
- data/lib/hoodoo/client/client.rb +6 -0
- data/lib/hoodoo/communicators/pool.rb +1 -1
- data/lib/hoodoo/services/services/context.rb +6 -0
- data/lib/hoodoo/services/services/request.rb +6 -6
- data/lib/hoodoo/version.rb +2 -2
- data/spec/active/active_record/secure_spec.rb +213 -0
- data/spec/active/active_record/security_helper_spec.rb +201 -0
- data/spec/active/active_record/writer_spec.rb +14 -0
- data/spec/client/client_spec.rb +11 -2
- data/spec/services/services/context_spec.rb +27 -13
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: cbae2491898cb113ffb0d3b9580e860e050d0fd5
|
4
|
+
data.tar.gz: d7dd9c51af9a0015572d9cb46d5a0fb63ee679c4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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"
|
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"
|
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
|
-
|
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
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
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
|
-
#
|
79
|
-
#
|
80
|
-
#
|
81
|
-
#
|
82
|
-
#
|
83
|
-
#
|
84
|
-
#
|
85
|
-
#
|
86
|
-
#
|
87
|
-
#
|
88
|
-
#
|
89
|
-
#
|
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
|
-
#
|
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
|
-
#
|
120
|
-
#
|
121
|
-
#
|
122
|
-
#
|
123
|
-
#
|
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
|
-
#
|
180
|
-
#
|
181
|
-
#
|
182
|
-
#
|
183
|
-
#
|
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
|
-
#
|
220
|
-
#
|
221
|
-
#
|
222
|
-
#
|
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.
|
342
|
+
instance = self.new_in( context, attributes )
|
313
343
|
instance.persist_in( context )
|
314
344
|
|
315
345
|
return instance
|