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