protected_attributes 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. data/.gitignore +17 -0
  2. data/.travis.yml +17 -0
  3. data/Gemfile +7 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +111 -0
  6. data/Rakefile +11 -0
  7. data/lib/action_controller/accessible_params_wrapper.rb +29 -0
  8. data/lib/active_model/mass_assignment_security.rb +353 -0
  9. data/lib/active_model/mass_assignment_security/permission_set.rb +40 -0
  10. data/lib/active_model/mass_assignment_security/sanitizer.rb +74 -0
  11. data/lib/active_record/mass_assignment_security.rb +23 -0
  12. data/lib/active_record/mass_assignment_security/associations.rb +116 -0
  13. data/lib/active_record/mass_assignment_security/attribute_assignment.rb +88 -0
  14. data/lib/active_record/mass_assignment_security/core.rb +27 -0
  15. data/lib/active_record/mass_assignment_security/inheritance.rb +18 -0
  16. data/lib/active_record/mass_assignment_security/nested_attributes.rb +148 -0
  17. data/lib/active_record/mass_assignment_security/persistence.rb +81 -0
  18. data/lib/active_record/mass_assignment_security/reflection.rb +9 -0
  19. data/lib/active_record/mass_assignment_security/relation.rb +47 -0
  20. data/lib/active_record/mass_assignment_security/validations.rb +24 -0
  21. data/lib/protected_attributes.rb +14 -0
  22. data/lib/protected_attributes/railtie.rb +18 -0
  23. data/lib/protected_attributes/version.rb +3 -0
  24. data/protected_attributes.gemspec +26 -0
  25. data/test/abstract_unit.rb +156 -0
  26. data/test/accessible_params_wrapper_test.rb +76 -0
  27. data/test/ar_helper.rb +67 -0
  28. data/test/attribute_sanitization_test.rb +929 -0
  29. data/test/mass_assignment_security/black_list_test.rb +20 -0
  30. data/test/mass_assignment_security/permission_set_test.rb +36 -0
  31. data/test/mass_assignment_security/sanitizer_test.rb +50 -0
  32. data/test/mass_assignment_security/white_list_test.rb +19 -0
  33. data/test/mass_assignment_security_test.rb +118 -0
  34. data/test/models/company.rb +105 -0
  35. data/test/models/keyboard.rb +3 -0
  36. data/test/models/mass_assignment_specific.rb +76 -0
  37. data/test/models/person.rb +82 -0
  38. data/test/models/subscriber.rb +5 -0
  39. data/test/models/task.rb +5 -0
  40. data/test/test_helper.rb +3 -0
  41. 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