protected_attributes 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|