Skip to content
This repository
Browse code

AM mass assignment security attr_accessible and attr_protected now al…

…low for scopes using :as => scope eg.

    
    attr_accessible :name
    attr_accessible :name, :admin, :as => :admin
  • Loading branch information...
commit 1054ebd613c5596bc1ebb8d610d19e5fa374cca5 1 parent af1b489
Josh Kalderimis authored April 23, 2011
119  activemodel/lib/active_model/mass_assignment_security.rb
@@ -24,10 +24,7 @@ module MassAssignmentSecurity
24 24
     #     include ActiveModel::MassAssignmentSecurity
25 25
     #
26 26
     #     attr_accessible :first_name, :last_name
27  
-    #
28  
-    #     def self.admin_accessible_attributes
29  
-    #       accessible_attributes + [ :plan_id ]
30  
-    #     end
  27
+    #     attr_accessible :first_name, :last_name, :plan_id, :as => :admin
31 28
     #
32 29
     #     def update
33 30
     #       ...
@@ -37,19 +34,18 @@ module MassAssignmentSecurity
37 34
     #
38 35
     #     protected
39 36
     #
40  
-    #     def account_params
41  
-    #       sanitize_for_mass_assignment(params[:account])
42  
-    #     end
43  
-    #
44  
-    #     def mass_assignment_authorizer
45  
-    #       admin ? admin_accessible_attributes : super
  37
+    #     def scope
  38
+    #       scope = admin ? :admin : :default
  39
+    #       sanitize_for_mass_assignment(params[:account], scope)
46 40
     #     end
47 41
     #
48 42
     #   end
49 43
     #
50 44
     module ClassMethods
51 45
       # Attributes named in this macro are protected from mass-assignment
52  
-      # whenever attributes are sanitized before assignment.
  46
+      # whenever attributes are sanitized before assignment. A scope for the
  47
+      # attributes is optional, if no scope is provided then :default is used.
  48
+      # A scope can be defined by using the :as option.
53 49
       #
54 50
       # Mass-assignment to these attributes will simply be ignored, to assign
55 51
       # to them you can use direct writer methods. This is meant to protect
@@ -60,36 +56,58 @@ module ClassMethods
60 56
       #     include ActiveModel::MassAssignmentSecurity
61 57
       #
62 58
       #     attr_accessor :name, :credit_rating
63  
-      #     attr_protected :credit_rating
64 59
       #
65  
-      #     def attributes=(values)
66  
-      #       sanitize_for_mass_assignment(values).each do |k, v|
  60
+      #     attr_protected :credit_rating, :last_login
  61
+      #     attr_protected :last_login, :as => :admin
  62
+      #
  63
+      #     def assign_attributes(values, options = {})
  64
+      #       sanitize_for_mass_assignment(values, options[:as]).each do |k, v|
67 65
       #         send("#{k}=", v)
68 66
       #       end
69 67
       #     end
70 68
       #   end
71 69
       #
  70
+      # When using a :default scope :
  71
+      #
72 72
       #   customer = Customer.new
73  
-      #   customer.attributes = { "name" => "David", "credit_rating" => "Excellent" }
  73
+      #   customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :default)
74 74
       #   customer.name          # => "David"
75 75
       #   customer.credit_rating # => nil
  76
+      #   customer.last_login    # => nil
76 77
       #
77 78
       #   customer.credit_rating = "Average"
78 79
       #   customer.credit_rating # => "Average"
79 80
       #
  81
+      # And using the :admin scope :
  82
+      #
  83
+      #   customer = Customer.new
  84
+      #   customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :admin)
  85
+      #   customer.name          # => "David"
  86
+      #   customer.credit_rating # => "Excellent"
  87
+      #   customer.last_login    # => nil
  88
+      #
80 89
       # To start from an all-closed default and enable attributes as needed,
81 90
       # have a look at +attr_accessible+.
82 91
       #
83 92
       # Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of +attr_protected+
84 93
       # to sanitize attributes won't provide sufficient protection.
85  
-      def attr_protected(*names)
86  
-        self._protected_attributes = self.protected_attributes + names
  94
+      def attr_protected(*args)
  95
+        options = args.extract_options!
  96
+        scope = options[:as] || :default
  97
+
  98
+        self._protected_attributes        = protected_attributes_configs.dup
  99
+        self._protected_attributes[scope] = self.protected_attributes(scope) + args
  100
+
87 101
         self._active_authorizer = self._protected_attributes
88 102
       end
89 103
 
90 104
       # Specifies a white list of model attributes that can be set via
91 105
       # mass-assignment.
92 106
       #
  107
+      # Like +attr_protected+, a scope for the attributes is optional,
  108
+      # if no scope is provided then :default is used. A scope can be defined by
  109
+      # using the :as option.
  110
+      #
93 111
       # This is the opposite of the +attr_protected+ macro: Mass-assignment
94 112
       # will only set attributes in this list, to assign to the rest of
95 113
       # attributes you can use direct writer methods. This is meant to protect
@@ -102,57 +120,90 @@ def attr_protected(*names)
102 120
       #     include ActiveModel::MassAssignmentSecurity
103 121
       #
104 122
       #     attr_accessor :name, :credit_rating
  123
+      #
105 124
       #     attr_accessible :name
  125
+      #     attr_accessible :name, :credit_rating, :as => :admin
106 126
       #
107  
-      #     def attributes=(values)
108  
-      #       sanitize_for_mass_assignment(values).each do |k, v|
  127
+      #     def assign_attributes(values, options = {})
  128
+      #       sanitize_for_mass_assignment(values, options[:as]).each do |k, v|
109 129
       #         send("#{k}=", v)
110 130
       #       end
111 131
       #     end
112 132
       #   end
113 133
       #
  134
+      # When using a :default scope :
  135
+      #
114 136
       #   customer = Customer.new
115  
-      #   customer.attributes = { :name => "David", :credit_rating => "Excellent" }
  137
+      #   customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :default)
116 138
       #   customer.name          # => "David"
117 139
       #   customer.credit_rating # => nil
118 140
       #
119 141
       #   customer.credit_rating = "Average"
120 142
       #   customer.credit_rating # => "Average"
121 143
       #
  144
+      # And using the :admin scope :
  145
+      #
  146
+      #   customer = Customer.new
  147
+      #   customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :admin)
  148
+      #   customer.name          # => "David"
  149
+      #   customer.credit_rating # => "Excellent"
  150
+      #
122 151
       # Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of +attr_accessible+
123 152
       # to sanitize attributes won't provide sufficient protection.
124  
-      def attr_accessible(*names)
125  
-        self._accessible_attributes = self.accessible_attributes + names
  153
+      def attr_accessible(*args)
  154
+        options = args.extract_options!
  155
+        scope = options[:as] || :default
  156
+
  157
+        self._accessible_attributes        = accessible_attributes_configs.dup
  158
+        self._accessible_attributes[scope] = self.accessible_attributes(scope) + args
  159
+
126 160
         self._active_authorizer = self._accessible_attributes
127 161
       end
128 162
 
129  
-      def protected_attributes
130  
-        self._protected_attributes ||= BlackList.new(attributes_protected_by_default).tap do |w|
131  
-          w.logger = self.logger if self.respond_to?(:logger)
132  
-        end
  163
+      def protected_attributes(scope = :default)
  164
+        protected_attributes_configs[scope]
133 165
       end
134 166
 
135  
-      def accessible_attributes
136  
-        self._accessible_attributes ||= WhiteList.new.tap { |w| w.logger = self.logger if self.respond_to?(:logger) }
  167
+      def accessible_attributes(scope = :default)
  168
+        accessible_attributes_configs[scope]
137 169
       end
138 170
 
139  
-      def active_authorizer
140  
-        self._active_authorizer ||= protected_attributes
  171
+      def active_authorizers
  172
+        self._active_authorizer ||= protected_attributes_configs
141 173
       end
  174
+      alias active_authorizer active_authorizers
142 175
 
143 176
       def attributes_protected_by_default
144 177
         []
145 178
       end
  179
+
  180
+      private
  181
+
  182
+      def protected_attributes_configs
  183
+        self._protected_attributes ||= begin
  184
+          default_black_list = BlackList.new(attributes_protected_by_default).tap do |w|
  185
+            w.logger = self.logger if self.respond_to?(:logger)
  186
+          end
  187
+          Hash.new(default_black_list)
  188
+        end
  189
+      end
  190
+
  191
+      def accessible_attributes_configs
  192
+        self._accessible_attributes ||= begin
  193
+          default_white_list = WhiteList.new.tap { |w| w.logger = self.logger if self.respond_to?(:logger) }
  194
+          Hash.new(default_white_list)
  195
+        end
  196
+      end
146 197
     end
147 198
 
148 199
   protected
149 200
 
150  
-    def sanitize_for_mass_assignment(attributes)
151  
-      mass_assignment_authorizer.sanitize(attributes)
  201
+    def sanitize_for_mass_assignment(attributes, scope = :default)
  202
+      mass_assignment_authorizer(scope).sanitize(attributes)
152 203
     end
153 204
 
154  
-    def mass_assignment_authorizer
155  
-      self.class.active_authorizer
  205
+    def mass_assignment_authorizer(scope = :default)
  206
+      self.class.active_authorizer[scope]
156 207
     end
157 208
   end
158 209
 end
39  activemodel/test/cases/mass_assignment_security_test.rb
@@ -10,10 +10,27 @@ def test_attribute_protection
10 10
     assert_equal expected, sanitized
11 11
   end
12 12
 
  13
+  def test_only_moderator_scope_attribute_accessible
  14
+    user = SpecialUser.new
  15
+    expected = { "name" => "John Smith", "email" => "john@smith.com" }
  16
+    sanitized = user.sanitize_for_mass_assignment(expected.merge("admin" => true), :moderator)
  17
+    assert_equal expected, sanitized
  18
+
  19
+    sanitized = user.sanitize_for_mass_assignment({ "name" => "John Smith", "email" => "john@smith.com", "admin" => true })
  20
+    assert_equal({}, sanitized)
  21
+  end
  22
+
13 23
   def test_attributes_accessible
14 24
     user = Person.new
15 25
     expected = { "name" => "John Smith", "email" => "john@smith.com" }
16  
-    sanitized = user.sanitize_for_mass_assignment(expected.merge("super_powers" => true))
  26
+    sanitized = user.sanitize_for_mass_assignment(expected.merge("admin" => true))
  27
+    assert_equal expected, sanitized
  28
+  end
  29
+
  30
+  def test_admin_scoped_attributes_accessible
  31
+    user = Person.new
  32
+    expected = { "name" => "John Smith", "email" => "john@smith.com", "admin" => true }
  33
+    sanitized = user.sanitize_for_mass_assignment(expected.merge("super_powers" => true), :admin)
17 34
     assert_equal expected, sanitized
18 35
   end
19 36
 
@@ -26,20 +43,30 @@ def test_attributes_protected_by_default
26 43
 
27 44
   def test_mass_assignment_protection_inheritance
28 45
     assert_blank LoosePerson.accessible_attributes
29  
-    assert_equal Set.new([ 'credit_rating', 'administrator']), LoosePerson.protected_attributes
  46
+    assert_equal Set.new(['credit_rating', 'administrator']), LoosePerson.protected_attributes
  47
+
  48
+    assert_blank LoosePerson.accessible_attributes
  49
+    assert_equal Set.new(['credit_rating']), LoosePerson.protected_attributes(:admin)
30 50
 
31 51
     assert_blank LooseDescendant.accessible_attributes
32  
-    assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number']), LooseDescendant.protected_attributes
  52
+    assert_equal Set.new(['credit_rating', 'administrator', 'phone_number']), LooseDescendant.protected_attributes
33 53
 
34 54
     assert_blank LooseDescendantSecond.accessible_attributes
35  
-    assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number', 'name']), LooseDescendantSecond.protected_attributes,
  55
+    assert_equal Set.new(['credit_rating', 'administrator', 'phone_number', 'name']), LooseDescendantSecond.protected_attributes,
36 56
       'Running attr_protected twice in one class should merge the protections'
37 57
 
38 58
     assert_blank TightPerson.protected_attributes - TightPerson.attributes_protected_by_default
39  
-    assert_equal Set.new([ 'name', 'address' ]), TightPerson.accessible_attributes
  59
+    assert_equal Set.new(['name', 'address']), TightPerson.accessible_attributes
  60
+
  61
+    assert_blank TightPerson.protected_attributes(:admin) - TightPerson.attributes_protected_by_default
  62
+    assert_equal Set.new(['name', 'address', 'admin']), TightPerson.accessible_attributes(:admin)
40 63
 
41 64
     assert_blank TightDescendant.protected_attributes - TightDescendant.attributes_protected_by_default
42  
-    assert_equal Set.new([ 'name', 'address', 'phone_number' ]), TightDescendant.accessible_attributes
  65
+    assert_equal Set.new(['name', 'address', 'phone_number']), TightDescendant.accessible_attributes
  66
+
  67
+    assert_blank TightDescendant.protected_attributes(:admin) - TightDescendant.attributes_protected_by_default
  68
+    assert_equal Set.new(['name', 'address', 'admin', 'super_powers']), TightDescendant.accessible_attributes(:admin)
  69
+
43 70
   end
44 71
 
45 72
   def test_mass_assignment_multiparameter_protector
11  activemodel/test/cases/secure_password_test.rb
@@ -45,13 +45,14 @@ class SecurePasswordTest < ActiveModel::TestCase
45 45
   end
46 46
 
47 47
   test "visitor#password_digest should be protected against mass assignment" do
48  
-    assert Visitor.active_authorizer.kind_of?(ActiveModel::MassAssignmentSecurity::BlackList)
49  
-    assert Visitor.active_authorizer.include?(:password_digest)
  48
+    assert Visitor.active_authorizers[:default].kind_of?(ActiveModel::MassAssignmentSecurity::BlackList)
  49
+    assert Visitor.active_authorizers[:default].include?(:password_digest)
50 50
   end
51 51
 
52 52
   test "Administrator's mass_assignment_authorizer should be WhiteList" do
53  
-    assert Administrator.active_authorizer.kind_of?(ActiveModel::MassAssignmentSecurity::WhiteList)
54  
-    assert !Administrator.active_authorizer.include?(:password_digest)
55  
-    assert Administrator.active_authorizer.include?(:name)
  53
+    active_authorizer = Administrator.active_authorizers[:default]
  54
+    assert active_authorizer.kind_of?(ActiveModel::MassAssignmentSecurity::WhiteList)
  55
+    assert !active_authorizer.include?(:password_digest)
  56
+    assert active_authorizer.include?(:name)
56 57
   end
57 58
 end
11  activemodel/test/models/mass_assignment_specific.rb
@@ -5,9 +5,17 @@ class User
5 5
   public :sanitize_for_mass_assignment
6 6
 end
7 7
 
  8
+class SpecialUser
  9
+  include ActiveModel::MassAssignmentSecurity
  10
+  attr_accessible :name, :email, :as => :moderator
  11
+
  12
+  public :sanitize_for_mass_assignment
  13
+end
  14
+
8 15
 class Person
9 16
   include ActiveModel::MassAssignmentSecurity
10 17
   attr_accessible :name, :email
  18
+  attr_accessible :name, :email, :admin, :as => :admin
11 19
 
12 20
   public :sanitize_for_mass_assignment
13 21
 end
@@ -32,6 +40,7 @@ class Task
32 40
 class LoosePerson
33 41
   include ActiveModel::MassAssignmentSecurity
34 42
   attr_protected :credit_rating, :administrator
  43
+  attr_protected :credit_rating, :as => :admin
35 44
 end
36 45
 
37 46
 class LooseDescendant < LoosePerson
@@ -46,6 +55,7 @@ class LooseDescendantSecond< LoosePerson
46 55
 class TightPerson
47 56
   include ActiveModel::MassAssignmentSecurity
48 57
   attr_accessible :name, :address
  58
+  attr_accessible :name, :address, :admin, :as => :admin
49 59
 
50 60
   def self.attributes_protected_by_default
51 61
     ["mobile_number"]
@@ -54,4 +64,5 @@ def self.attributes_protected_by_default
54 64
 
55 65
 class TightDescendant < TightPerson
56 66
   attr_accessible :phone_number
  67
+  attr_accessible :super_powers, :as => :admin
57 68
 end

0 notes on commit 1054ebd

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