diff --git a/activemodel/lib/active_model/mass_assignment_security.rb b/activemodel/lib/active_model/mass_assignment_security.rb index be48415739f4a84cffda865aa3b7398a6835707f..e3299331759dccb77d32fb61ebcbfb6658a168b2 100644 --- a/activemodel/lib/active_model/mass_assignment_security.rb +++ b/activemodel/lib/active_model/mass_assignment_security.rb @@ -24,10 +24,7 @@ module MassAssignmentSecurity # include ActiveModel::MassAssignmentSecurity # # attr_accessible :first_name, :last_name - # - # def self.admin_accessible_attributes - # accessible_attributes + [ :plan_id ] - # end + # attr_accessible :first_name, :last_name, :plan_id, :as => :admin # # def update # ... @@ -37,19 +34,18 @@ module MassAssignmentSecurity # # protected # - # def account_params - # sanitize_for_mass_assignment(params[:account]) - # end - # - # def mass_assignment_authorizer - # admin ? admin_accessible_attributes : super + # def scope + # scope = admin ? :admin : :default + # sanitize_for_mass_assignment(params[:account], scope) # end # # end # module ClassMethods # Attributes named in this macro are protected from mass-assignment - # whenever attributes are sanitized before assignment. + # whenever attributes are sanitized before assignment. A scope for the + # attributes is optional, if no scope is provided then :default is used. + # A scope can be defined by using the :as option. # # Mass-assignment to these attributes will simply be ignored, to assign # to them you can use direct writer methods. This is meant to protect @@ -60,36 +56,58 @@ module ClassMethods # include ActiveModel::MassAssignmentSecurity # # attr_accessor :name, :credit_rating - # attr_protected :credit_rating # - # def attributes=(values) - # sanitize_for_mass_assignment(values).each do |k, v| + # attr_protected :credit_rating, :last_login + # attr_protected :last_login, :as => :admin + # + # def assign_attributes(values, options = {}) + # sanitize_for_mass_assignment(values, options[:as]).each do |k, v| # send("#{k}=", v) # end # end # end # + # When using a :default scope : + # # customer = Customer.new - # customer.attributes = { "name" => "David", "credit_rating" => "Excellent" } + # customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :default) # customer.name # => "David" # customer.credit_rating # => nil + # customer.last_login # => nil # # customer.credit_rating = "Average" # customer.credit_rating # => "Average" # + # And using the :admin scope : + # + # customer = Customer.new + # customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :admin) + # customer.name # => "David" + # customer.credit_rating # => "Excellent" + # customer.last_login # => nil + # # To start from an all-closed default and enable attributes as needed, # have a look at +attr_accessible+. # # Note that using Hash#except or Hash#slice in place of +attr_protected+ # to sanitize attributes won't provide sufficient protection. - def attr_protected(*names) - self._protected_attributes = self.protected_attributes + names + def attr_protected(*args) + options = args.extract_options! + scope = options[:as] || :default + + self._protected_attributes = protected_attributes_configs.dup + self._protected_attributes[scope] = self.protected_attributes(scope) + args + self._active_authorizer = self._protected_attributes end # Specifies a white list of model attributes that can be set via # mass-assignment. # + # Like +attr_protected+, a scope for the attributes is optional, + # if no scope is provided then :default is used. A scope can be defined by + # using the :as option. + # # This is the opposite of the +attr_protected+ macro: Mass-assignment # will only set attributes in this list, to assign to the rest of # attributes you can use direct writer methods. This is meant to protect @@ -102,57 +120,90 @@ def attr_protected(*names) # include ActiveModel::MassAssignmentSecurity # # attr_accessor :name, :credit_rating + # # attr_accessible :name + # attr_accessible :name, :credit_rating, :as => :admin # - # def attributes=(values) - # sanitize_for_mass_assignment(values).each do |k, v| + # def assign_attributes(values, options = {}) + # sanitize_for_mass_assignment(values, options[:as]).each do |k, v| # send("#{k}=", v) # end # end # end # + # When using a :default scope : + # # customer = Customer.new - # customer.attributes = { :name => "David", :credit_rating => "Excellent" } + # customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :default) # customer.name # => "David" # customer.credit_rating # => nil # # customer.credit_rating = "Average" # customer.credit_rating # => "Average" # + # And using the :admin scope : + # + # customer = Customer.new + # customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :admin) + # customer.name # => "David" + # customer.credit_rating # => "Excellent" + # # Note that using Hash#except or Hash#slice in place of +attr_accessible+ # to sanitize attributes won't provide sufficient protection. - def attr_accessible(*names) - self._accessible_attributes = self.accessible_attributes + names + def attr_accessible(*args) + options = args.extract_options! + scope = options[:as] || :default + + self._accessible_attributes = accessible_attributes_configs.dup + self._accessible_attributes[scope] = self.accessible_attributes(scope) + args + self._active_authorizer = self._accessible_attributes end - def protected_attributes - self._protected_attributes ||= BlackList.new(attributes_protected_by_default).tap do |w| - w.logger = self.logger if self.respond_to?(:logger) - end + def protected_attributes(scope = :default) + protected_attributes_configs[scope] end - def accessible_attributes - self._accessible_attributes ||= WhiteList.new.tap { |w| w.logger = self.logger if self.respond_to?(:logger) } + def accessible_attributes(scope = :default) + accessible_attributes_configs[scope] end - def active_authorizer - self._active_authorizer ||= protected_attributes + def active_authorizers + self._active_authorizer ||= protected_attributes_configs end + alias active_authorizer active_authorizers def attributes_protected_by_default [] end + + private + + def protected_attributes_configs + self._protected_attributes ||= begin + default_black_list = BlackList.new(attributes_protected_by_default).tap do |w| + w.logger = self.logger if self.respond_to?(:logger) + end + Hash.new(default_black_list) + end + end + + def accessible_attributes_configs + self._accessible_attributes ||= begin + default_white_list = WhiteList.new.tap { |w| w.logger = self.logger if self.respond_to?(:logger) } + Hash.new(default_white_list) + end + end end protected - def sanitize_for_mass_assignment(attributes) - mass_assignment_authorizer.sanitize(attributes) + def sanitize_for_mass_assignment(attributes, scope = :default) + mass_assignment_authorizer(scope).sanitize(attributes) end - def mass_assignment_authorizer - self.class.active_authorizer + def mass_assignment_authorizer(scope = :default) + self.class.active_authorizer[scope] end end end diff --git a/activemodel/test/cases/mass_assignment_security_test.rb b/activemodel/test/cases/mass_assignment_security_test.rb index f84e55e8d97dd33f9cfd96cf976d5b4398b00090..b22ce874ea65dae920c2f528654e09e0e8a84585 100644 --- a/activemodel/test/cases/mass_assignment_security_test.rb +++ b/activemodel/test/cases/mass_assignment_security_test.rb @@ -10,10 +10,27 @@ def test_attribute_protection assert_equal expected, sanitized end + def test_only_moderator_scope_attribute_accessible + user = SpecialUser.new + expected = { "name" => "John Smith", "email" => "john@smith.com" } + sanitized = user.sanitize_for_mass_assignment(expected.merge("admin" => true), :moderator) + assert_equal expected, sanitized + + sanitized = user.sanitize_for_mass_assignment({ "name" => "John Smith", "email" => "john@smith.com", "admin" => true }) + assert_equal({}, sanitized) + end + def test_attributes_accessible user = Person.new expected = { "name" => "John Smith", "email" => "john@smith.com" } - sanitized = user.sanitize_for_mass_assignment(expected.merge("super_powers" => true)) + sanitized = user.sanitize_for_mass_assignment(expected.merge("admin" => true)) + assert_equal expected, sanitized + end + + def test_admin_scoped_attributes_accessible + user = Person.new + expected = { "name" => "John Smith", "email" => "john@smith.com", "admin" => true } + sanitized = user.sanitize_for_mass_assignment(expected.merge("super_powers" => true), :admin) assert_equal expected, sanitized end @@ -26,20 +43,30 @@ def test_attributes_protected_by_default def test_mass_assignment_protection_inheritance assert_blank LoosePerson.accessible_attributes - assert_equal Set.new([ 'credit_rating', 'administrator']), LoosePerson.protected_attributes + assert_equal Set.new(['credit_rating', 'administrator']), LoosePerson.protected_attributes + + assert_blank LoosePerson.accessible_attributes + assert_equal Set.new(['credit_rating']), LoosePerson.protected_attributes(:admin) assert_blank LooseDescendant.accessible_attributes - assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number']), LooseDescendant.protected_attributes + assert_equal Set.new(['credit_rating', 'administrator', 'phone_number']), LooseDescendant.protected_attributes assert_blank LooseDescendantSecond.accessible_attributes - assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number', 'name']), LooseDescendantSecond.protected_attributes, + assert_equal Set.new(['credit_rating', 'administrator', 'phone_number', 'name']), LooseDescendantSecond.protected_attributes, 'Running attr_protected twice in one class should merge the protections' assert_blank TightPerson.protected_attributes - TightPerson.attributes_protected_by_default - assert_equal Set.new([ 'name', 'address' ]), TightPerson.accessible_attributes + assert_equal Set.new(['name', 'address']), TightPerson.accessible_attributes + + assert_blank TightPerson.protected_attributes(:admin) - TightPerson.attributes_protected_by_default + assert_equal Set.new(['name', 'address', 'admin']), TightPerson.accessible_attributes(:admin) assert_blank TightDescendant.protected_attributes - TightDescendant.attributes_protected_by_default - assert_equal Set.new([ 'name', 'address', 'phone_number' ]), TightDescendant.accessible_attributes + assert_equal Set.new(['name', 'address', 'phone_number']), TightDescendant.accessible_attributes + + assert_blank TightDescendant.protected_attributes(:admin) - TightDescendant.attributes_protected_by_default + assert_equal Set.new(['name', 'address', 'admin', 'super_powers']), TightDescendant.accessible_attributes(:admin) + end def test_mass_assignment_multiparameter_protector diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb index c455cf57b3adfe98aeaf02701cca97eeef16de02..6950c3be1f79535a321002ac5739f6cf1ed7c2ab 100644 --- a/activemodel/test/cases/secure_password_test.rb +++ b/activemodel/test/cases/secure_password_test.rb @@ -45,13 +45,14 @@ class SecurePasswordTest < ActiveModel::TestCase end test "visitor#password_digest should be protected against mass assignment" do - assert Visitor.active_authorizer.kind_of?(ActiveModel::MassAssignmentSecurity::BlackList) - assert Visitor.active_authorizer.include?(:password_digest) + assert Visitor.active_authorizers[:default].kind_of?(ActiveModel::MassAssignmentSecurity::BlackList) + assert Visitor.active_authorizers[:default].include?(:password_digest) end test "Administrator's mass_assignment_authorizer should be WhiteList" do - assert Administrator.active_authorizer.kind_of?(ActiveModel::MassAssignmentSecurity::WhiteList) - assert !Administrator.active_authorizer.include?(:password_digest) - assert Administrator.active_authorizer.include?(:name) + active_authorizer = Administrator.active_authorizers[:default] + assert active_authorizer.kind_of?(ActiveModel::MassAssignmentSecurity::WhiteList) + assert !active_authorizer.include?(:password_digest) + assert active_authorizer.include?(:name) end end diff --git a/activemodel/test/models/mass_assignment_specific.rb b/activemodel/test/models/mass_assignment_specific.rb index 2a8fe170c2286cc5c2dc1901a313eb805cf24936..53b37369ff62b96942fb24ceee145f4b3642f089 100644 --- a/activemodel/test/models/mass_assignment_specific.rb +++ b/activemodel/test/models/mass_assignment_specific.rb @@ -5,9 +5,17 @@ class User public :sanitize_for_mass_assignment end +class SpecialUser + include ActiveModel::MassAssignmentSecurity + attr_accessible :name, :email, :as => :moderator + + public :sanitize_for_mass_assignment +end + class Person include ActiveModel::MassAssignmentSecurity attr_accessible :name, :email + attr_accessible :name, :email, :admin, :as => :admin public :sanitize_for_mass_assignment end @@ -32,6 +40,7 @@ class Task class LoosePerson include ActiveModel::MassAssignmentSecurity attr_protected :credit_rating, :administrator + attr_protected :credit_rating, :as => :admin end class LooseDescendant < LoosePerson @@ -46,6 +55,7 @@ class LooseDescendantSecond< LoosePerson class TightPerson include ActiveModel::MassAssignmentSecurity attr_accessible :name, :address + attr_accessible :name, :address, :admin, :as => :admin def self.attributes_protected_by_default ["mobile_number"] @@ -54,4 +64,5 @@ def self.attributes_protected_by_default class TightDescendant < TightPerson attr_accessible :phone_number + attr_accessible :super_powers, :as => :admin end \ No newline at end of file