protected_attributes_continued 1.2.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.
@@ -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,75 @@
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(klass, attrs) is intended to be overwritten by a subclass"
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.backtrace_cleaner
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
+ unless (attrs - insensitive_attributes).empty?
60
+ raise ActiveModel::MassAssignmentSecurity::Error.new(klass, attrs)
61
+ end
62
+ end
63
+
64
+ def insensitive_attributes
65
+ ['id']
66
+ end
67
+ end
68
+
69
+ class Error < StandardError #:nodoc:
70
+ def initialize(klass, attrs)
71
+ super("Can't mass-assign protected attributes for #{klass.name}: #{attrs.join(', ')}")
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,29 @@
1
+ require "active_record"
2
+
3
+ def active_record_40?
4
+ ActiveRecord::VERSION::MAJOR == 4 && ActiveRecord::VERSION::MINOR == 0
5
+ end
6
+
7
+ require "active_record/mass_assignment_security/associations"
8
+ require "active_record/mass_assignment_security/attribute_assignment"
9
+ require "active_record/mass_assignment_security/core"
10
+ require "active_record/mass_assignment_security/nested_attributes"
11
+ require "active_record/mass_assignment_security/persistence"
12
+ require "active_record/mass_assignment_security/reflection"
13
+ require "active_record/mass_assignment_security/relation"
14
+ require "active_record/mass_assignment_security/validations"
15
+ require "active_record/mass_assignment_security/associations"
16
+ require "active_record/mass_assignment_security/inheritance"
17
+
18
+ class ActiveRecord::Base
19
+ include ActiveRecord::MassAssignmentSecurity::Core
20
+ include ActiveRecord::MassAssignmentSecurity::AttributeAssignment
21
+ include ActiveRecord::MassAssignmentSecurity::Persistence
22
+ include ActiveRecord::MassAssignmentSecurity::Validations
23
+ include ActiveRecord::MassAssignmentSecurity::NestedAttributes
24
+ include ActiveRecord::MassAssignmentSecurity::Inheritance
25
+ end
26
+
27
+ class ActiveRecord::SchemaMigration < ActiveRecord::Base
28
+ attr_accessible :version
29
+ end
@@ -0,0 +1,154 @@
1
+ module ActiveRecord
2
+ module Associations
3
+ class Association
4
+ undef :build_record
5
+
6
+ def build_record(attributes, options)
7
+ reflection.build_association(attributes, options) do |record|
8
+ attributes = create_scope.except(*(record.changed - [reflection.foreign_key]))
9
+ record.assign_attributes(attributes, without_protection: true)
10
+ end
11
+ end
12
+
13
+ private :build_record
14
+ end
15
+
16
+ class CollectionAssociation
17
+ undef :build
18
+ undef :create
19
+ undef :create!
20
+
21
+ def build(attributes = {}, options = {}, &block)
22
+ if attributes.is_a?(Array)
23
+ attributes.collect { |attr| build(attr, options, &block) }
24
+ else
25
+ add_to_target(build_record(attributes, options)) do |record|
26
+ yield(record) if block_given?
27
+ end
28
+ end
29
+ end
30
+
31
+ def create(attributes = {}, options = {}, &block)
32
+ create_record(attributes, options, &block)
33
+ end
34
+
35
+ def create!(attributes = {}, options = {}, &block)
36
+ create_record(attributes, options, true, &block)
37
+ end
38
+
39
+ def create_record(attributes, options, raise = false, &block)
40
+ unless owner.persisted?
41
+ raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
42
+ end
43
+
44
+ if attributes.is_a?(Array)
45
+ attributes.collect { |attr| create_record(attr, options, raise, &block) }
46
+ else
47
+ transaction do
48
+ add_to_target(build_record(attributes, options)) do |record|
49
+ yield(record) if block_given?
50
+ insert_record(record, true, raise)
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ private :create_record
57
+ end
58
+
59
+ class CollectionProxy
60
+ undef :create
61
+ undef :create!
62
+
63
+ def build(attributes = {}, options = {}, &block)
64
+ @association.build(attributes, options, &block)
65
+ end
66
+ alias_method :new, :build
67
+
68
+ def create(attributes = {}, options = {}, &block)
69
+ @association.create(attributes, options, &block)
70
+ end
71
+
72
+ def create!(attributes = {}, options = {}, &block)
73
+ @association.create!(attributes, options, &block)
74
+ end
75
+ end
76
+
77
+ module ThroughAssociation
78
+ undef :build_record if respond_to?(:build_record, false)
79
+
80
+ private
81
+
82
+ def build_record(attributes, options={})
83
+ inverse = source_reflection.inverse_of
84
+ target = through_association.target
85
+
86
+ if inverse && target && !target.is_a?(Array)
87
+ attributes[inverse.foreign_key] = target.id
88
+ end
89
+
90
+ super(attributes, options)
91
+ end
92
+ end
93
+
94
+ class HasManyThroughAssociation
95
+ undef :build_record
96
+ undef :options_for_through_record if respond_to?(:options_for_through_record, false)
97
+
98
+ def build_record(attributes, options = {})
99
+ ensure_not_nested
100
+
101
+ record = super(attributes, options)
102
+
103
+ inverse = source_reflection.inverse_of
104
+ if inverse
105
+ if inverse.macro == :has_many
106
+ record.send(inverse.name) << build_through_record(record)
107
+ elsif inverse.macro == :has_one
108
+ record.send("#{inverse.name}=", build_through_record(record))
109
+ end
110
+ end
111
+
112
+ record
113
+ end
114
+ private :build_record
115
+
116
+ def options_for_through_record
117
+ [through_scope_attributes, without_protection: true]
118
+ end
119
+ private :options_for_through_record
120
+ end
121
+
122
+ class SingularAssociation
123
+ undef :create
124
+ undef :create!
125
+ undef :build
126
+
127
+ def create(attributes = {}, options = {}, &block)
128
+ create_record(attributes, options, &block)
129
+ end
130
+
131
+ def create!(attributes = {}, options = {}, &block)
132
+ create_record(attributes, options, true, &block)
133
+ end
134
+
135
+ def build(attributes = {}, options = {})
136
+ record = build_record(attributes, options)
137
+ yield(record) if block_given?
138
+ set_new_record(record)
139
+ record
140
+ end
141
+
142
+ def create_record(attributes, options = {}, raise_error = false)
143
+ record = build_record(attributes, options)
144
+ yield(record) if block_given?
145
+ saved = record.save
146
+ set_new_record(record)
147
+ raise RecordInvalid.new(record) if !saved && raise_error
148
+ record
149
+ end
150
+
151
+ private :create_record
152
+ end
153
+ end
154
+ 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,17 @@
1
+ module ActiveRecord
2
+ module MassAssignmentSecurity
3
+ module Core
4
+
5
+ private
6
+
7
+ def init_attributes(attributes, options)
8
+ assign_attributes(attributes, options)
9
+ end
10
+
11
+ def init_internals
12
+ super
13
+ @mass_assignment_options = nil
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,23 @@
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_attributes?(attrs)
13
+ active_authorizer[:default].deny?(inheritance_column) ? nil : super
14
+ end
15
+
16
+ # Support Active Record <= 4.0.3, which uses the old method signature.
17
+ def subclass_from_attrs(attrs)
18
+ active_authorizer[:default].deny?(inheritance_column) ? nil : super
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,157 @@
1
+ module ActiveRecord
2
+ module MassAssignmentSecurity
3
+ module NestedAttributes
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+
8
+ REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |key, value| key == '_destroy' || value.blank? } }
9
+
10
+ def accepts_nested_attributes_for(*attr_names)
11
+ options = { :allow_destroy => false, :update_only => false }
12
+ options.update(attr_names.extract_options!)
13
+ options.assert_valid_keys(:allow_destroy, :reject_if, :limit, :update_only)
14
+ options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank
15
+
16
+ attr_names.each do |association_name|
17
+ if reflection = reflect_on_association(association_name)
18
+ if active_record_40?
19
+ reflection.options[:autosave] = true
20
+ else
21
+ reflection.autosave = true
22
+ end
23
+ add_autosave_association_callbacks(reflection)
24
+
25
+ nested_attributes_options = self.nested_attributes_options.dup
26
+ nested_attributes_options[association_name.to_sym] = options
27
+ self.nested_attributes_options = nested_attributes_options
28
+
29
+ type = (reflection.collection? ? :collection : :one_to_one)
30
+
31
+ generated_methods_module = active_record_40? ? generated_feature_methods : generated_association_methods
32
+
33
+ # def pirate_attributes=(attributes)
34
+ # assign_nested_attributes_for_one_to_one_association(:pirate, attributes, mass_assignment_options)
35
+ # end
36
+ generated_methods_module.module_eval <<-eoruby, __FILE__, __LINE__ + 1
37
+ if method_defined?(:#{association_name}_attributes=)
38
+ remove_method(:#{association_name}_attributes=)
39
+ end
40
+ def #{association_name}_attributes=(attributes)
41
+ assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes, mass_assignment_options)
42
+ end
43
+ eoruby
44
+ else
45
+ raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ UNASSIGNABLE_KEYS = %w( id _destroy )
54
+
55
+ def assign_nested_attributes_for_one_to_one_association(association_name, attributes, assignment_opts = {})
56
+ options = self.nested_attributes_options[association_name]
57
+ attributes = attributes.with_indifferent_access
58
+
59
+ if (options[:update_only] || !attributes['id'].blank?) && (record = send(association_name)) &&
60
+ (options[:update_only] || record.id.to_s == attributes['id'].to_s)
61
+ assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy], assignment_opts) unless call_reject_if(association_name, attributes)
62
+
63
+ elsif attributes['id'].present? && !assignment_opts[:without_protection]
64
+ raise_nested_attributes_record_not_found!(association_name, attributes['id'])
65
+
66
+ elsif !reject_new_record?(association_name, attributes)
67
+ method = "build_#{association_name}"
68
+ if respond_to?(method)
69
+ send(method, attributes.except(*unassignable_keys(assignment_opts)), assignment_opts)
70
+ else
71
+ raise ArgumentError, "Cannot build association `#{association_name}'. Are you trying to build a polymorphic one-to-one association?"
72
+ end
73
+ end
74
+ end
75
+
76
+ def assign_nested_attributes_for_collection_association(association_name, attributes_collection, assignment_opts = {})
77
+ options = self.nested_attributes_options[association_name]
78
+
79
+ unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
80
+ raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
81
+ end
82
+
83
+ if limit = options[:limit]
84
+ limit = case limit
85
+ when Symbol
86
+ send(limit)
87
+ when Proc
88
+ limit.call
89
+ else
90
+ limit
91
+ end
92
+
93
+ if limit && attributes_collection.size > limit
94
+ raise ::ActiveRecord::NestedAttributes::TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead."
95
+ end
96
+ end
97
+
98
+ if attributes_collection.is_a? Hash
99
+ keys = attributes_collection.keys
100
+ attributes_collection = if keys.include?('id') || keys.include?(:id)
101
+ [attributes_collection]
102
+ else
103
+ attributes_collection.values
104
+ end
105
+ end
106
+
107
+ association = association(association_name)
108
+
109
+ existing_records = if association.loaded?
110
+ association.target
111
+ else
112
+ attribute_ids = attributes_collection.map {|a| a['id'] || a[:id] }.compact
113
+ attribute_ids.empty? ? [] : association.scope.where(association.klass.primary_key => attribute_ids)
114
+ end
115
+
116
+ attributes_collection.each do |attributes|
117
+ attributes = attributes.with_indifferent_access
118
+
119
+ if attributes['id'].blank?
120
+ unless reject_new_record?(association_name, attributes)
121
+ association.build(attributes.except(*unassignable_keys(assignment_opts)), assignment_opts)
122
+ end
123
+ elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s }
124
+ unless association.loaded? || call_reject_if(association_name, attributes)
125
+ # Make sure we are operating on the actual object which is in the association's
126
+ # proxy_target array (either by finding it, or adding it if not found)
127
+ target_record = association.target.detect { |record| record == existing_record }
128
+
129
+ if target_record
130
+ existing_record = target_record
131
+ else
132
+ association.add_to_target(existing_record)
133
+ end
134
+ end
135
+
136
+ if !call_reject_if(association_name, attributes)
137
+ assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy], assignment_opts)
138
+ end
139
+ elsif assignment_opts[:without_protection]
140
+ association.build(attributes.except(*unassignable_keys(assignment_opts)), assignment_opts)
141
+ else
142
+ raise_nested_attributes_record_not_found!(association_name, attributes['id'])
143
+ end
144
+ end
145
+ end
146
+
147
+ def assign_to_or_mark_for_destruction(record, attributes, allow_destroy, assignment_opts)
148
+ record.assign_attributes(attributes.except(*unassignable_keys(assignment_opts)), assignment_opts)
149
+ record.mark_for_destruction if has_destroy_flag?(attributes) && allow_destroy
150
+ end
151
+
152
+ def unassignable_keys(assignment_opts)
153
+ assignment_opts[:without_protection] ? UNASSIGNABLE_KEYS - %w[id] : UNASSIGNABLE_KEYS
154
+ end
155
+ end
156
+ end
157
+ end