protected_attributes 1.0.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.
- data/.gitignore +17 -0
- data/.travis.yml +17 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +111 -0
- data/Rakefile +11 -0
- data/lib/action_controller/accessible_params_wrapper.rb +29 -0
- data/lib/active_model/mass_assignment_security.rb +353 -0
- data/lib/active_model/mass_assignment_security/permission_set.rb +40 -0
- data/lib/active_model/mass_assignment_security/sanitizer.rb +74 -0
- data/lib/active_record/mass_assignment_security.rb +23 -0
- data/lib/active_record/mass_assignment_security/associations.rb +116 -0
- data/lib/active_record/mass_assignment_security/attribute_assignment.rb +88 -0
- data/lib/active_record/mass_assignment_security/core.rb +27 -0
- data/lib/active_record/mass_assignment_security/inheritance.rb +18 -0
- data/lib/active_record/mass_assignment_security/nested_attributes.rb +148 -0
- data/lib/active_record/mass_assignment_security/persistence.rb +81 -0
- data/lib/active_record/mass_assignment_security/reflection.rb +9 -0
- data/lib/active_record/mass_assignment_security/relation.rb +47 -0
- data/lib/active_record/mass_assignment_security/validations.rb +24 -0
- data/lib/protected_attributes.rb +14 -0
- data/lib/protected_attributes/railtie.rb +18 -0
- data/lib/protected_attributes/version.rb +3 -0
- data/protected_attributes.gemspec +26 -0
- data/test/abstract_unit.rb +156 -0
- data/test/accessible_params_wrapper_test.rb +76 -0
- data/test/ar_helper.rb +67 -0
- data/test/attribute_sanitization_test.rb +929 -0
- data/test/mass_assignment_security/black_list_test.rb +20 -0
- data/test/mass_assignment_security/permission_set_test.rb +36 -0
- data/test/mass_assignment_security/sanitizer_test.rb +50 -0
- data/test/mass_assignment_security/white_list_test.rb +19 -0
- data/test/mass_assignment_security_test.rb +118 -0
- data/test/models/company.rb +105 -0
- data/test/models/keyboard.rb +3 -0
- data/test/models/mass_assignment_specific.rb +76 -0
- data/test/models/person.rb +82 -0
- data/test/models/subscriber.rb +5 -0
- data/test/models/task.rb +5 -0
- data/test/test_helper.rb +3 -0
- metadata +199 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module ActiveModel
|
4
|
+
module MassAssignmentSecurity
|
5
|
+
class PermissionSet < Set #:nodoc:
|
6
|
+
|
7
|
+
def +(values)
|
8
|
+
super(values.compact.map(&:to_s))
|
9
|
+
end
|
10
|
+
|
11
|
+
def include?(key)
|
12
|
+
super(remove_multiparameter_id(key))
|
13
|
+
end
|
14
|
+
|
15
|
+
def deny?(key)
|
16
|
+
raise NotImplementedError, "#deny?(key) supposed to be overwritten"
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
def remove_multiparameter_id(key)
|
22
|
+
key.to_s.gsub(/\(.+/, '')
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class WhiteList < PermissionSet #:nodoc:
|
27
|
+
|
28
|
+
def deny?(key)
|
29
|
+
!include?(key)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class BlackList < PermissionSet #:nodoc:
|
34
|
+
|
35
|
+
def deny?(key)
|
36
|
+
include?(key)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module ActiveModel
|
2
|
+
module MassAssignmentSecurity
|
3
|
+
class Sanitizer #:nodoc:
|
4
|
+
# Returns all attributes not denied by the authorizer.
|
5
|
+
def sanitize(klass, attributes, authorizer)
|
6
|
+
rejected = []
|
7
|
+
sanitized_attributes = attributes.reject do |key, value|
|
8
|
+
rejected << key if authorizer.deny?(key)
|
9
|
+
end
|
10
|
+
process_removed_attributes(klass, rejected) unless rejected.empty?
|
11
|
+
sanitized_attributes
|
12
|
+
end
|
13
|
+
|
14
|
+
protected
|
15
|
+
|
16
|
+
def process_removed_attributes(klass, attrs)
|
17
|
+
raise NotImplementedError, "#process_removed_attributes(attrs) suppose to be overwritten"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class LoggerSanitizer < Sanitizer #:nodoc:
|
22
|
+
def initialize(target)
|
23
|
+
@target = target
|
24
|
+
super()
|
25
|
+
end
|
26
|
+
|
27
|
+
def logger
|
28
|
+
@target.logger
|
29
|
+
end
|
30
|
+
|
31
|
+
def logger?
|
32
|
+
@target.respond_to?(:logger) && @target.logger
|
33
|
+
end
|
34
|
+
|
35
|
+
def backtrace
|
36
|
+
if defined? Rails
|
37
|
+
Rails.backtrace_cleaner.clean(caller)
|
38
|
+
else
|
39
|
+
caller
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def process_removed_attributes(klass, attrs)
|
44
|
+
if logger?
|
45
|
+
logger.warn do
|
46
|
+
"WARNING: Can't mass-assign protected attributes for #{klass.name}: #{attrs.join(', ')}\n" +
|
47
|
+
backtrace.map { |trace| "\t#{trace}" }.join("\n")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class StrictSanitizer < Sanitizer #:nodoc:
|
54
|
+
def initialize(target = nil)
|
55
|
+
super()
|
56
|
+
end
|
57
|
+
|
58
|
+
def process_removed_attributes(klass, attrs)
|
59
|
+
return if (attrs - insensitive_attributes).empty?
|
60
|
+
raise ActiveModel::MassAssignmentSecurity::Error.new(klass, attrs)
|
61
|
+
end
|
62
|
+
|
63
|
+
def insensitive_attributes
|
64
|
+
['id']
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class Error < StandardError #:nodoc:
|
69
|
+
def initialize(klass, attrs)
|
70
|
+
super("Can't mass-assign protected attributes for #{klass.name}: #{attrs.join(', ')}")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require "active_record"
|
2
|
+
require "active_record/mass_assignment_security/associations"
|
3
|
+
require "active_record/mass_assignment_security/attribute_assignment"
|
4
|
+
require "active_record/mass_assignment_security/core"
|
5
|
+
require "active_record/mass_assignment_security/nested_attributes"
|
6
|
+
require "active_record/mass_assignment_security/persistence"
|
7
|
+
require "active_record/mass_assignment_security/reflection"
|
8
|
+
require "active_record/mass_assignment_security/relation"
|
9
|
+
require "active_record/mass_assignment_security/validations"
|
10
|
+
require "active_record/mass_assignment_security/associations"
|
11
|
+
require "active_record/mass_assignment_security/inheritance"
|
12
|
+
|
13
|
+
class ActiveRecord::Base
|
14
|
+
include ActiveRecord::MassAssignmentSecurity::Core
|
15
|
+
include ActiveRecord::MassAssignmentSecurity::AttributeAssignment
|
16
|
+
include ActiveRecord::MassAssignmentSecurity::Persistence
|
17
|
+
include ActiveRecord::MassAssignmentSecurity::Relation
|
18
|
+
include ActiveRecord::MassAssignmentSecurity::Validations
|
19
|
+
include ActiveRecord::MassAssignmentSecurity::NestedAttributes
|
20
|
+
include ActiveRecord::MassAssignmentSecurity::Inheritance
|
21
|
+
end
|
22
|
+
|
23
|
+
ActiveRecord::SchemaMigration.attr_accessible(:version)
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Associations
|
3
|
+
class Association
|
4
|
+
def build_record(attributes, options)
|
5
|
+
reflection.build_association(attributes, options) do |record|
|
6
|
+
attributes = create_scope.except(*(record.changed - [reflection.foreign_key]))
|
7
|
+
record.assign_attributes(attributes, without_protection: true)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
private :build_record
|
12
|
+
end
|
13
|
+
|
14
|
+
class CollectionAssociation
|
15
|
+
def build(attributes = {}, options = {}, &block)
|
16
|
+
if attributes.is_a?(Array)
|
17
|
+
attributes.collect { |attr| build(attr, options, &block) }
|
18
|
+
else
|
19
|
+
add_to_target(build_record(attributes, options)) do |record|
|
20
|
+
yield(record) if block_given?
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def create(attributes = {}, options = {}, &block)
|
26
|
+
create_record(attributes, options, &block)
|
27
|
+
end
|
28
|
+
|
29
|
+
def create!(attributes = {}, options = {}, &block)
|
30
|
+
create_record(attributes, options, true, &block)
|
31
|
+
end
|
32
|
+
|
33
|
+
def create_record(attributes, options, raise = false, &block)
|
34
|
+
unless owner.persisted?
|
35
|
+
raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
|
36
|
+
end
|
37
|
+
|
38
|
+
if attributes.is_a?(Array)
|
39
|
+
attributes.collect { |attr| create_record(attr, options, raise, &block) }
|
40
|
+
else
|
41
|
+
transaction do
|
42
|
+
add_to_target(build_record(attributes, options)) do |record|
|
43
|
+
yield(record) if block_given?
|
44
|
+
insert_record(record, true, raise)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private :create_record
|
51
|
+
end
|
52
|
+
|
53
|
+
class CollectionProxy
|
54
|
+
def build(attributes = {}, options = {}, &block)
|
55
|
+
@association.build(attributes, options, &block)
|
56
|
+
end
|
57
|
+
|
58
|
+
def create(attributes = {}, options = {}, &block)
|
59
|
+
@association.create(attributes, options, &block)
|
60
|
+
end
|
61
|
+
|
62
|
+
def create!(attributes = {}, options = {}, &block)
|
63
|
+
@association.create!(attributes, options, &block)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class HasManyThroughAssociation
|
68
|
+
def build_record(attributes, options = {})
|
69
|
+
ensure_not_nested
|
70
|
+
|
71
|
+
record = super(attributes, options)
|
72
|
+
|
73
|
+
inverse = source_reflection.inverse_of
|
74
|
+
if inverse
|
75
|
+
if inverse.macro == :has_many
|
76
|
+
record.send(inverse.name) << build_through_record(record)
|
77
|
+
elsif inverse.macro == :has_one
|
78
|
+
record.send("#{inverse.name}=", build_through_record(record))
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
record
|
83
|
+
end
|
84
|
+
|
85
|
+
private :build_record
|
86
|
+
end
|
87
|
+
|
88
|
+
class SingularAssociation
|
89
|
+
def create(attributes = {}, options = {}, &block)
|
90
|
+
create_record(attributes, options, &block)
|
91
|
+
end
|
92
|
+
|
93
|
+
def create!(attributes = {}, options = {}, &block)
|
94
|
+
create_record(attributes, options, true, &block)
|
95
|
+
end
|
96
|
+
|
97
|
+
def build(attributes = {}, options = {})
|
98
|
+
record = build_record(attributes, options)
|
99
|
+
yield(record) if block_given?
|
100
|
+
set_new_record(record)
|
101
|
+
record
|
102
|
+
end
|
103
|
+
|
104
|
+
def create_record(attributes, options = {}, raise_error = false)
|
105
|
+
record = build_record(attributes, options)
|
106
|
+
yield(record) if block_given?
|
107
|
+
saved = record.save
|
108
|
+
set_new_record(record)
|
109
|
+
raise RecordInvalid.new(record) if !saved && raise_error
|
110
|
+
record
|
111
|
+
end
|
112
|
+
|
113
|
+
private :create_record
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'active_model/mass_assignment_security'
|
2
|
+
require 'active_record'
|
3
|
+
|
4
|
+
module ActiveRecord
|
5
|
+
module MassAssignmentSecurity
|
6
|
+
module AttributeAssignment
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
include ActiveModel::MassAssignmentSecurity
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
private
|
12
|
+
|
13
|
+
# The primary key and inheritance column can never be set by mass-assignment for security reasons.
|
14
|
+
def attributes_protected_by_default
|
15
|
+
default = [ primary_key, inheritance_column ]
|
16
|
+
default << 'id' unless primary_key.eql? 'id'
|
17
|
+
default
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Allows you to set all the attributes for a particular mass-assignment
|
22
|
+
# security role by passing in a hash of attributes with keys matching
|
23
|
+
# the attribute names (which again matches the column names) and the role
|
24
|
+
# name using the :as option.
|
25
|
+
#
|
26
|
+
# To bypass mass-assignment security you can use the :without_protection => true
|
27
|
+
# option.
|
28
|
+
#
|
29
|
+
# class User < ActiveRecord::Base
|
30
|
+
# attr_accessible :name
|
31
|
+
# attr_accessible :name, :is_admin, :as => :admin
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# user = User.new
|
35
|
+
# user.assign_attributes({ :name => 'Josh', :is_admin => true })
|
36
|
+
# user.name # => "Josh"
|
37
|
+
# user.is_admin? # => false
|
38
|
+
#
|
39
|
+
# user = User.new
|
40
|
+
# user.assign_attributes({ :name => 'Josh', :is_admin => true }, :as => :admin)
|
41
|
+
# user.name # => "Josh"
|
42
|
+
# user.is_admin? # => true
|
43
|
+
#
|
44
|
+
# user = User.new
|
45
|
+
# user.assign_attributes({ :name => 'Josh', :is_admin => true }, :without_protection => true)
|
46
|
+
# user.name # => "Josh"
|
47
|
+
# user.is_admin? # => true
|
48
|
+
def assign_attributes(new_attributes, options = {})
|
49
|
+
return if new_attributes.blank?
|
50
|
+
|
51
|
+
attributes = new_attributes.stringify_keys
|
52
|
+
multi_parameter_attributes = []
|
53
|
+
nested_parameter_attributes = []
|
54
|
+
previous_options = @mass_assignment_options
|
55
|
+
@mass_assignment_options = options
|
56
|
+
|
57
|
+
unless options[:without_protection]
|
58
|
+
attributes = sanitize_for_mass_assignment(attributes, mass_assignment_role)
|
59
|
+
end
|
60
|
+
|
61
|
+
attributes.each do |k, v|
|
62
|
+
if k.include?("(")
|
63
|
+
multi_parameter_attributes << [ k, v ]
|
64
|
+
elsif v.is_a?(Hash)
|
65
|
+
nested_parameter_attributes << [ k, v ]
|
66
|
+
else
|
67
|
+
_assign_attribute(k, v)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
assign_nested_parameter_attributes(nested_parameter_attributes) unless nested_parameter_attributes.empty?
|
72
|
+
assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty?
|
73
|
+
ensure
|
74
|
+
@mass_assignment_options = previous_options
|
75
|
+
end
|
76
|
+
|
77
|
+
protected
|
78
|
+
|
79
|
+
def mass_assignment_options
|
80
|
+
@mass_assignment_options ||= {}
|
81
|
+
end
|
82
|
+
|
83
|
+
def mass_assignment_role
|
84
|
+
mass_assignment_options[:as] || :default
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module MassAssignmentSecurity
|
3
|
+
module Core
|
4
|
+
def initialize(attributes = nil, options = {})
|
5
|
+
defaults = self.class.column_defaults.dup
|
6
|
+
defaults.each { |k, v| defaults[k] = v.dup if v.duplicable? }
|
7
|
+
|
8
|
+
@attributes = self.class.initialize_attributes(defaults)
|
9
|
+
@columns_hash = self.class.column_types.dup
|
10
|
+
|
11
|
+
init_internals
|
12
|
+
ensure_proper_type
|
13
|
+
populate_with_current_scope_attributes
|
14
|
+
|
15
|
+
assign_attributes(attributes, options) if attributes
|
16
|
+
|
17
|
+
yield self if block_given?
|
18
|
+
run_callbacks :initialize unless _initialize_callbacks.empty?
|
19
|
+
end
|
20
|
+
|
21
|
+
def init_internals
|
22
|
+
super
|
23
|
+
@mass_assignment_options = nil
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module MassAssignmentSecurity
|
3
|
+
module Inheritance
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
private
|
8
|
+
# Detect the subclass from the inheritance column of attrs. If the inheritance column value
|
9
|
+
# is not self or a valid subclass, raises ActiveRecord::SubclassNotFound
|
10
|
+
# If this is a StrongParameters hash, and access to inheritance_column is not permitted,
|
11
|
+
# this will ignore the inheritance column and return nil
|
12
|
+
def subclass_from_attrs(attrs)
|
13
|
+
active_authorizer[:default].deny?(inheritance_column) ? nil : super
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module MassAssignmentSecurity
|
3
|
+
module NestedAttributes
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
def accepts_nested_attributes_for(*attr_names)
|
8
|
+
options = { :allow_destroy => false, :update_only => false }
|
9
|
+
options.update(attr_names.extract_options!)
|
10
|
+
options.assert_valid_keys(:allow_destroy, :reject_if, :limit, :update_only)
|
11
|
+
options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank
|
12
|
+
|
13
|
+
attr_names.each do |association_name|
|
14
|
+
if reflection = reflect_on_association(association_name)
|
15
|
+
reflection.options[:autosave] = true
|
16
|
+
add_autosave_association_callbacks(reflection)
|
17
|
+
|
18
|
+
nested_attributes_options = self.nested_attributes_options.dup
|
19
|
+
nested_attributes_options[association_name.to_sym] = options
|
20
|
+
self.nested_attributes_options = nested_attributes_options
|
21
|
+
|
22
|
+
type = (reflection.collection? ? :collection : :one_to_one)
|
23
|
+
|
24
|
+
# def pirate_attributes=(attributes)
|
25
|
+
# assign_nested_attributes_for_one_to_one_association(:pirate, attributes, mass_assignment_options)
|
26
|
+
# end
|
27
|
+
generated_feature_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1
|
28
|
+
if method_defined?(:#{association_name}_attributes=)
|
29
|
+
remove_method(:#{association_name}_attributes=)
|
30
|
+
end
|
31
|
+
def #{association_name}_attributes=(attributes)
|
32
|
+
assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes, mass_assignment_options)
|
33
|
+
end
|
34
|
+
eoruby
|
35
|
+
else
|
36
|
+
raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
UNASSIGNABLE_KEYS = %w( id _destroy )
|
45
|
+
|
46
|
+
def assign_nested_attributes_for_one_to_one_association(association_name, attributes, assignment_opts = {})
|
47
|
+
options = self.nested_attributes_options[association_name]
|
48
|
+
attributes = attributes.with_indifferent_access
|
49
|
+
|
50
|
+
if (options[:update_only] || !attributes['id'].blank?) && (record = send(association_name)) &&
|
51
|
+
(options[:update_only] || record.id.to_s == attributes['id'].to_s)
|
52
|
+
assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy], assignment_opts) unless call_reject_if(association_name, attributes)
|
53
|
+
|
54
|
+
elsif attributes['id'].present? && !assignment_opts[:without_protection]
|
55
|
+
raise_nested_attributes_record_not_found(association_name, attributes['id'])
|
56
|
+
|
57
|
+
elsif !reject_new_record?(association_name, attributes)
|
58
|
+
method = "build_#{association_name}"
|
59
|
+
if respond_to?(method)
|
60
|
+
send(method, attributes.except(*unassignable_keys(assignment_opts)), assignment_opts)
|
61
|
+
else
|
62
|
+
raise ArgumentError, "Cannot build association `#{association_name}'. Are you trying to build a polymorphic one-to-one association?"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def assign_nested_attributes_for_collection_association(association_name, attributes_collection, assignment_opts = {})
|
68
|
+
options = self.nested_attributes_options[association_name]
|
69
|
+
|
70
|
+
unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
|
71
|
+
raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
|
72
|
+
end
|
73
|
+
|
74
|
+
if limit = options[:limit]
|
75
|
+
limit = case limit
|
76
|
+
when Symbol
|
77
|
+
send(limit)
|
78
|
+
when Proc
|
79
|
+
limit.call
|
80
|
+
else
|
81
|
+
limit
|
82
|
+
end
|
83
|
+
|
84
|
+
if limit && attributes_collection.size > limit
|
85
|
+
raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead."
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
if attributes_collection.is_a? Hash
|
90
|
+
keys = attributes_collection.keys
|
91
|
+
attributes_collection = if keys.include?('id') || keys.include?(:id)
|
92
|
+
[attributes_collection]
|
93
|
+
else
|
94
|
+
attributes_collection.values
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
association = association(association_name)
|
99
|
+
|
100
|
+
existing_records = if association.loaded?
|
101
|
+
association.target
|
102
|
+
else
|
103
|
+
attribute_ids = attributes_collection.map {|a| a['id'] || a[:id] }.compact
|
104
|
+
attribute_ids.empty? ? [] : association.scope.where(association.klass.primary_key => attribute_ids)
|
105
|
+
end
|
106
|
+
|
107
|
+
attributes_collection.each do |attributes|
|
108
|
+
attributes = attributes.with_indifferent_access
|
109
|
+
|
110
|
+
if attributes['id'].blank?
|
111
|
+
unless reject_new_record?(association_name, attributes)
|
112
|
+
association.build(attributes.except(*unassignable_keys(assignment_opts)), assignment_opts)
|
113
|
+
end
|
114
|
+
elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s }
|
115
|
+
unless association.loaded? || call_reject_if(association_name, attributes)
|
116
|
+
# Make sure we are operating on the actual object which is in the association's
|
117
|
+
# proxy_target array (either by finding it, or adding it if not found)
|
118
|
+
target_record = association.target.detect { |record| record == existing_record }
|
119
|
+
|
120
|
+
if target_record
|
121
|
+
existing_record = target_record
|
122
|
+
else
|
123
|
+
association.add_to_target(existing_record)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
if !call_reject_if(association_name, attributes)
|
128
|
+
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy], assignment_opts)
|
129
|
+
end
|
130
|
+
elsif assignment_opts[:without_protection]
|
131
|
+
association.build(attributes.except(*unassignable_keys(assignment_opts)), assignment_opts)
|
132
|
+
else
|
133
|
+
raise_nested_attributes_record_not_found(association_name, attributes['id'])
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def assign_to_or_mark_for_destruction(record, attributes, allow_destroy, assignment_opts)
|
139
|
+
record.assign_attributes(attributes.except(*unassignable_keys(assignment_opts)), assignment_opts)
|
140
|
+
record.mark_for_destruction if has_destroy_flag?(attributes) && allow_destroy
|
141
|
+
end
|
142
|
+
|
143
|
+
def unassignable_keys(assignment_opts)
|
144
|
+
assignment_opts[:without_protection] ? UNASSIGNABLE_KEYS - %w[id] : UNASSIGNABLE_KEYS
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|