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