Skip to content
This repository
Browse code

Added mass-assignment security :as and :without_protection support to…

… AR.new and AR.create
  • Loading branch information...
commit 7c5ae0a88fc9406857ee362c827c57eb23fd5f95 1 parent b8ccd05
Josh Kalderimis joshk authored
33 activerecord/lib/active_record/base.rb
@@ -475,10 +475,19 @@ def find_by_sql(sql, binds = [])
475 475 # The +attributes+ parameter can be either be a Hash or an Array of Hashes. These Hashes describe the
476 476 # attributes on the objects that are to be created.
477 477 #
  478 + # +create+ respects mass-assignment security and accepts either +:as+ or +:without_protection+ options
  479 + # in the +options+ parameter.
  480 + #
478 481 # ==== Examples
479 482 # # Create a single new object
480 483 # User.create(:first_name => 'Jamie')
481 484 #
  485 + # # Create a single new object using the :admin mass-assignment security scope
  486 + # User.create({ :first_name => 'Jamie', :is_admin => true }, :as => :admin)
  487 + #
  488 + # # Create a single new object bypassing mass-assignment security
  489 + # User.create({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true)
  490 + #
482 491 # # Create an Array of new objects
483 492 # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }])
484 493 #
@@ -491,11 +500,11 @@ def find_by_sql(sql, binds = [])
491 500 # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }]) do |u|
492 501 # u.is_admin = false
493 502 # end
494   - def create(attributes = nil, &block)
  503 + def create(attributes = nil, options = {}, &block)
495 504 if attributes.is_a?(Array)
496   - attributes.collect { |attr| create(attr, &block) }
  505 + attributes.collect { |attr| create(attr, options, &block) }
497 506 else
498   - object = new(attributes)
  507 + object = new(attributes, options)
499 508 yield(object) if block_given?
500 509 object.save
501 510 object
@@ -1484,7 +1493,20 @@ def encode_quoted_value(value) #:nodoc:
1484 1493 # attributes but not yet saved (pass a hash with key names matching the associated table column names).
1485 1494 # In both instances, valid attribute keys are determined by the column names of the associated table --
1486 1495 # hence you can't have attributes that aren't part of the table columns.
1487   - def initialize(attributes = nil)
  1496 + #
  1497 + # +initialize+ respects mass-assignment security and accepts either +:as+ or +:without_protection+ options
  1498 + # in the +options+ parameter.
  1499 + #
  1500 + # ==== Examples
  1501 + # # Instantiates a single new object
  1502 + # User.new(:first_name => 'Jamie')
  1503 + #
  1504 + # # Instantiates a single new object using the :admin mass-assignment security scope
  1505 + # User.new({ :first_name => 'Jamie', :is_admin => true }, :as => :admin)
  1506 + #
  1507 + # # Instantiates a single new object bypassing mass-assignment security
  1508 + # User.new({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true)
  1509 + def initialize(attributes = nil, options = {})
1488 1510 @attributes = attributes_from_column_definition
1489 1511 @association_cache = {}
1490 1512 @aggregation_cache = {}
@@ -1500,7 +1522,8 @@ def initialize(attributes = nil)
1500 1522 set_serialized_attributes
1501 1523
1502 1524 populate_with_current_scope_attributes
1503   - self.attributes = attributes unless attributes.nil?
  1525 +
  1526 + assign_attributes(attributes, options) if attributes
1504 1527
1505 1528 result = yield self if block_given?
1506 1529 run_callbacks :initialize
138 activerecord/test/cases/mass_assignment_security_test.rb
@@ -7,6 +7,12 @@
7 7
8 8 class MassAssignmentSecurityTest < ActiveRecord::TestCase
9 9
  10 + def setup
  11 + # another AR test modifies the columns which causes issues with create calls
  12 + TightPerson.reset_column_information
  13 + LoosePerson.reset_column_information
  14 + end
  15 +
10 16 def test_customized_primary_key_remains_protected
11 17 subscriber = Subscriber.new(:nick => 'webster123', :name => 'nice try')
12 18 assert_nil subscriber.id
@@ -35,60 +41,114 @@ def test_assign_attributes_uses_default_scope_when_no_scope_is_provided
35 41 p = LoosePerson.new
36 42 p.assign_attributes(attributes_hash)
37 43
38   - assert_equal nil, p.id
39   - assert_equal 'Josh', p.first_name
40   - assert_equal 'm', p.gender
41   - assert_equal nil, p.comments
  44 + assert_default_attributes(p)
42 45 end
43 46
44 47 def test_assign_attributes_skips_mass_assignment_security_protection_when_without_protection_is_used
45 48 p = LoosePerson.new
46 49 p.assign_attributes(attributes_hash, :without_protection => true)
47 50
48   - assert_equal 5, p.id
49   - assert_equal 'Josh', p.first_name
50   - assert_equal 'm', p.gender
51   - assert_equal 'rides a sweet bike', p.comments
  51 + assert_all_attributes(p)
52 52 end
53 53
54 54 def test_assign_attributes_with_default_scope_and_attr_protected_attributes
55 55 p = LoosePerson.new
56 56 p.assign_attributes(attributes_hash, :as => :default)
57 57
58   - assert_equal nil, p.id
59   - assert_equal 'Josh', p.first_name
60   - assert_equal 'm', p.gender
61   - assert_equal nil, p.comments
  58 + assert_default_attributes(p)
62 59 end
63 60
64 61 def test_assign_attributes_with_admin_scope_and_attr_protected_attributes
65 62 p = LoosePerson.new
66 63 p.assign_attributes(attributes_hash, :as => :admin)
67 64
68   - assert_equal nil, p.id
69   - assert_equal 'Josh', p.first_name
70   - assert_equal 'm', p.gender
71   - assert_equal 'rides a sweet bike', p.comments
  65 + assert_admin_attributes(p)
72 66 end
73 67
74 68 def test_assign_attributes_with_default_scope_and_attr_accessible_attributes
75 69 p = TightPerson.new
76 70 p.assign_attributes(attributes_hash, :as => :default)
77 71
78   - assert_equal nil, p.id
79   - assert_equal 'Josh', p.first_name
80   - assert_equal 'm', p.gender
81   - assert_equal nil, p.comments
  72 + assert_default_attributes(p)
82 73 end
83 74
84 75 def test_assign_attributes_with_admin_scope_and_attr_accessible_attributes
85 76 p = TightPerson.new
86 77 p.assign_attributes(attributes_hash, :as => :admin)
87 78
88   - assert_equal nil, p.id
89   - assert_equal 'Josh', p.first_name
90   - assert_equal 'm', p.gender
91   - assert_equal 'rides a sweet bike', p.comments
  79 + assert_admin_attributes(p)
  80 + end
  81 +
  82 + def test_new_with_attr_accessible_attributes
  83 + p = TightPerson.new(attributes_hash)
  84 +
  85 + assert_default_attributes(p)
  86 + end
  87 +
  88 + def test_new_with_attr_protected_attributes
  89 + p = LoosePerson.new(attributes_hash)
  90 +
  91 + assert_default_attributes(p)
  92 + end
  93 +
  94 + def test_create_with_attr_accessible_attributes
  95 + p = TightPerson.create(attributes_hash)
  96 +
  97 + assert_default_attributes(p, true)
  98 + end
  99 +
  100 + def test_create_with_attr_protected_attributes
  101 + p = LoosePerson.create(attributes_hash)
  102 +
  103 + assert_default_attributes(p, true)
  104 + end
  105 +
  106 + def test_new_with_admin_scope_with_attr_accessible_attributes
  107 + p = TightPerson.new(attributes_hash, :as => :admin)
  108 +
  109 + assert_admin_attributes(p)
  110 + end
  111 +
  112 + def test_new_with_admin_scope_with_attr_protected_attributes
  113 + p = LoosePerson.new(attributes_hash, :as => :admin)
  114 +
  115 + assert_admin_attributes(p)
  116 + end
  117 +
  118 + def test_create_with_admin_scope_with_attr_accessible_attributes
  119 + p = TightPerson.create(attributes_hash, :as => :admin)
  120 +
  121 + assert_admin_attributes(p, true)
  122 + end
  123 +
  124 + def test_create_with_admin_scope_with_attr_protected_attributes
  125 + p = LoosePerson.create(attributes_hash, :as => :admin)
  126 +
  127 + assert_admin_attributes(p, true)
  128 + end
  129 +
  130 + def test_new_with_without_protection_with_attr_accessible_attributes
  131 + p = TightPerson.new(attributes_hash, :without_protection => true)
  132 +
  133 + assert_all_attributes(p)
  134 + end
  135 +
  136 + def test_new_with_without_protection_with_attr_protected_attributes
  137 + p = LoosePerson.new(attributes_hash, :without_protection => true)
  138 +
  139 + assert_all_attributes(p)
  140 + end
  141 +
  142 + def test_create_with_without_protection_with_attr_accessible_attributes
  143 + p = TightPerson.create(attributes_hash, :without_protection => true)
  144 +
  145 + assert_all_attributes(p)
  146 + end
  147 +
  148 + def test_create_with_without_protection_with_attr_protected_attributes
  149 + p = LoosePerson.create(attributes_hash, :without_protection => true)
  150 +
  151 + assert_all_attributes(p)
92 152 end
93 153
94 154 def test_protection_against_class_attribute_writers
@@ -111,4 +171,34 @@ def attributes_hash
111 171 :comments => 'rides a sweet bike'
112 172 }
113 173 end
  174 +
  175 + def assert_default_attributes(person, create = false)
  176 + unless create
  177 + assert_nil person.id
  178 + else
  179 + assert !!person.id
  180 + end
  181 + assert_equal 'Josh', person.first_name
  182 + assert_equal 'm', person.gender
  183 + assert_nil person.comments
  184 + end
  185 +
  186 + def assert_admin_attributes(person, create = false)
  187 + unless create
  188 + assert_nil person.id
  189 + else
  190 + assert !!person.id
  191 + end
  192 + assert_equal 'Josh', person.first_name
  193 + assert_equal 'm', person.gender
  194 + assert_equal 'rides a sweet bike', person.comments
  195 + end
  196 +
  197 + def assert_all_attributes(person)
  198 + assert_equal 5, person.id
  199 + assert_equal 'Josh', person.first_name
  200 + assert_equal 'm', person.gender
  201 + assert_equal 'rides a sweet bike', person.comments
  202 + end
  203 +
114 204 end

0 comments on commit 7c5ae0a

Andrew White

This is going to break everybody who has overridden initialize in their models and there isn't even a mention of it in the CHANGELOG.

Josh Kalderimis

I'll get this fixed up right now, thanks for spotting that Andrew!

Josh Kalderimis

That is a good point, I will add better notes to the AR changelog describing this change better. Thanks for spotting this.

Please sign in to comment.
Something went wrong with that request. Please try again.