diff options
Diffstat (limited to 'activemodel/lib/active_model/mass_assignment_security.rb')
-rw-r--r-- | activemodel/lib/active_model/mass_assignment_security.rb | 283 |
1 files changed, 199 insertions, 84 deletions
diff --git a/activemodel/lib/active_model/mass_assignment_security.rb b/activemodel/lib/active_model/mass_assignment_security.rb index 3f9feb7631..f9841abcb0 100644 --- a/activemodel/lib/active_model/mass_assignment_security.rb +++ b/activemodel/lib/active_model/mass_assignment_security.rb @@ -1,80 +1,82 @@ -require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/string/inflections' -require 'active_support/core_ext/array/wrap' require 'active_model/mass_assignment_security/permission_set' require 'active_model/mass_assignment_security/sanitizer' module ActiveModel - # = Active Model Mass-Assignment Security + # == Active Model Mass-Assignment Security + # + # Mass assignment security provides an interface for protecting attributes + # from end-user assignment. For more complex permissions, mass assignment + # security may be handled outside the model by extending a non-ActiveRecord + # class, such as a controller, with this behavior. + # + # For example, a logged in user may need to assign additional attributes + # depending on their role: + # + # class AccountsController < ApplicationController + # include ActiveModel::MassAssignmentSecurity + # + # attr_accessible :first_name, :last_name + # attr_accessible :first_name, :last_name, :plan_id, as: :admin + # + # def update + # ... + # @account.update_attributes(account_params) + # ... + # end + # + # protected + # + # def account_params + # role = admin ? :admin : :default + # sanitize_for_mass_assignment(params[:account], role) + # end + # + # end + # + # === Configuration options + # + # * <tt>mass_assignment_sanitizer</tt> - Defines sanitize method. Possible + # values are: + # * <tt>:logger</tt> (default) - writes filtered attributes to logger + # * <tt>:strict</tt> - raise <tt>ActiveModel::MassAssignmentSecurity::Error</tt> + # on any protected attribute update. + # + # You can specify your own sanitizer object eg. <tt>MySanitizer.new</tt>. + # See <tt>ActiveModel::MassAssignmentSecurity::LoggerSanitizer</tt> for + # example implementation. module MassAssignmentSecurity extend ActiveSupport::Concern included do - class_attribute :_accessible_attributes - class_attribute :_protected_attributes - class_attribute :_active_authorizer + class_attribute :_accessible_attributes, instance_writer: false + class_attribute :_protected_attributes, instance_writer: false + class_attribute :_active_authorizer, instance_writer: false - class_attribute :_mass_assignment_sanitizer + class_attribute :_mass_assignment_sanitizer, instance_writer: false self.mass_assignment_sanitizer = :logger end - # Mass assignment security provides an interface for protecting attributes - # from end-user assignment. For more complex permissions, mass assignment security - # may be handled outside the model by extending a non-ActiveRecord class, - # such as a controller, with this behavior. - # - # For example, a logged in user may need to assign additional attributes depending - # on their role: - # - # class AccountsController < ApplicationController - # include ActiveModel::MassAssignmentSecurity - # - # attr_accessible :first_name, :last_name - # attr_accessible :first_name, :last_name, :plan_id, :as => :admin - # - # def update - # ... - # @account.update_attributes(account_params) - # ... - # end - # - # protected - # - # def account_params - # role = admin ? :admin : :default - # sanitize_for_mass_assignment(params[:account], role) - # end - # - # end - # - # = Configuration options - # - # * <tt>mass_assignment_sanitizer</tt> - Defines sanitize method. Possible values are: - # * <tt>:logger</tt> (default) - writes filtered attributes to logger - # * <tt>:strict</tt> - raise <tt>ActiveModel::MassAssignmentSecurity::Error</tt> on any protected attribute update - # - # You can specify your own sanitizer object eg. MySanitizer.new. - # See <tt>ActiveModel::MassAssignmentSecurity::LoggerSanitizer</tt> for example implementation. - # - # module ClassMethods # Attributes named in this macro are protected from mass-assignment # whenever attributes are sanitized before assignment. A role for the - # attributes is optional, if no role is provided then :default is used. - # A role can be defined by using the :as option. + # attributes is optional, if no role is provided then <tt>:default</tt> + # is used. A role can be defined by using the <tt>:as</tt> option with a + # symbol or an array of symbols as the value. # # Mass-assignment to these attributes will simply be ignored, to assign # to them you can use direct writer methods. This is meant to protect # sensitive attributes from being overwritten by malicious users - # tampering with URLs or forms. Example: + # tampering with URLs or forms. # # class Customer # include ActiveModel::MassAssignmentSecurity # - # attr_accessor :name, :credit_rating + # attr_accessor :name, :email, :logins_count # - # attr_protected :credit_rating, :last_login - # attr_protected :last_login, :as => :admin + # attr_protected :logins_count + # # Suppose that admin can not change email for customer + # attr_protected :logins_count, :email, as: :admin # # def assign_attributes(values, options = {}) # sanitize_for_mass_assignment(values, options[:as]).each do |k, v| @@ -83,37 +85,38 @@ module ActiveModel # end # end # - # When using the :default role : + # When using the <tt>:default</tt> role: # # customer = Customer.new - # 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" + # customer.assign_attributes({ name: 'David', email: 'a@b.com', logins_count: 5 }, as: :default) + # customer.name # => "David" + # customer.email # => "a@b.com" + # customer.logins_count # => nil # - # And using the :admin role : + # And using the <tt>:admin</tt> role: # # 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 + # customer.assign_attributes({ name: 'David', email: 'a@b.com', logins_count: 5}, as: :admin) + # customer.name # => "David" + # customer.email # => nil + # customer.logins_count # => nil + # + # customer.email = 'c@d.com' + # customer.email # => "c@d.com" # # To start from an all-closed default and enable attributes as needed, # have a look at +attr_accessible+. # - # Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of +attr_protected+ - # to sanitize attributes won't provide sufficient protection. + # Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of + # +attr_protected+ to sanitize attributes provides basically the same + # functionality, but it makes a bit tricky to deal with nested attributes. def attr_protected(*args) options = args.extract_options! role = options[:as] || :default self._protected_attributes = protected_attributes_configs.dup - Array.wrap(role).each do |name| + Array(role).each do |name| self._protected_attributes[name] = self.protected_attributes(name) + args end @@ -124,8 +127,9 @@ module ActiveModel # mass-assignment. # # Like +attr_protected+, a role for the attributes is optional, - # if no role is provided then :default is used. A role can be defined by - # using the :as option. + # if no role is provided then <tt>:default</tt> is used. A role can be + # defined by using the <tt>:as</tt> option with a symbol or an array of + # symbols as the value. # # This is the opposite of the +attr_protected+ macro: Mass-assignment # will only set attributes in this list, to assign to the rest of @@ -140,8 +144,10 @@ module ActiveModel # # attr_accessor :name, :credit_rating # - # attr_accessible :name - # attr_accessible :name, :credit_rating, :as => :admin + # # Both admin and default user can change name of a customer + # attr_accessible :name, as: [:admin, :default] + # # Only admin can change credit rating of a customer + # attr_accessible :credit_rating, as: :admin # # def assign_attributes(values, options = {}) # sanitize_for_mass_assignment(values, options[:as]).each do |k, v| @@ -150,55 +156,164 @@ module ActiveModel # end # end # - # When using the :default role : + # When using the <tt>:default</tt> role: # # customer = Customer.new - # customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :default) + # 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' # customer.credit_rating # => "Average" # - # And using the :admin role : + # And using the <tt>:admin</tt> role: # # customer = Customer.new - # customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :admin) + # 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 <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of +attr_accessible+ - # to sanitize attributes won't provide sufficient protection. + # Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of + # +attr_accessible+ to sanitize attributes provides basically the same + # functionality, but it makes a bit tricky to deal with nested attributes. def attr_accessible(*args) options = args.extract_options! role = options[:as] || :default self._accessible_attributes = accessible_attributes_configs.dup - Array.wrap(role).each do |name| + Array(role).each do |name| self._accessible_attributes[name] = self.accessible_attributes(name) + args end self._active_authorizer = self._accessible_attributes end + # Returns an instance of <tt>ActiveModel::MassAssignmentSecurity::BlackList</tt> + # with the attributes protected by #attr_protected method. If no +role+ + # is provided, then <tt>:default</tt> is used. + # + # class Customer + # include ActiveModel::MassAssignmentSecurity + # + # attr_accessor :name, :email, :logins_count + # + # attr_protected :logins_count + # attr_protected :logins_count, :email, as: :admin + # end + # + # Customer.protected_attributes + # # => #<ActiveModel::MassAssignmentSecurity::BlackList: {"logins_count"}> + # + # Customer.protected_attributes(:default) + # # => #<ActiveModel::MassAssignmentSecurity::BlackList: {"logins_count"}> + # + # Customer.protected_attributes(:admin) + # # => #<ActiveModel::MassAssignmentSecurity::BlackList: {"logins_count", "email"}> def protected_attributes(role = :default) protected_attributes_configs[role] end + # Returns an instance of <tt>ActiveModel::MassAssignmentSecurity::WhiteList</tt> + # with the attributes protected by #attr_accessible method. If no +role+ + # is provided, then <tt>:default</tt> is used. + # + # class Customer + # include ActiveModel::MassAssignmentSecurity + # + # attr_accessor :name, :credit_rating + # + # attr_accessible :name, as: [:admin, :default] + # attr_accessible :credit_rating, as: :admin + # end + # + # Customer.accessible_attributes + # # => #<ActiveModel::MassAssignmentSecurity::WhiteList: {"name"}> + # + # Customer.accessible_attributes(:default) + # # => #<ActiveModel::MassAssignmentSecurity::WhiteList: {"name"}> + # + # Customer.accessible_attributes(:admin) + # # => #<ActiveModel::MassAssignmentSecurity::WhiteList: {"name", "credit_rating"}> def accessible_attributes(role = :default) accessible_attributes_configs[role] end + # Returns a hash with the protected attributes (by #attr_accessible or + # #attr_protected) per role. + # + # class Customer + # include ActiveModel::MassAssignmentSecurity + # + # attr_accessor :name, :credit_rating + # + # attr_accessible :name, as: [:admin, :default] + # attr_accessible :credit_rating, as: :admin + # end + # + # Customer.active_authorizers + # # => { + # # :admin=> #<ActiveModel::MassAssignmentSecurity::WhiteList: {"name", "credit_rating"}>, + # # :default=>#<ActiveModel::MassAssignmentSecurity::WhiteList: {"name"}> + # # } def active_authorizers self._active_authorizer ||= protected_attributes_configs end alias active_authorizer active_authorizers + # Returns an empty array by default. You can still override this to define + # the default attributes protected by #attr_protected method. + # + # class Customer + # include ActiveModel::MassAssignmentSecurity + # + # def self.attributes_protected_by_default + # [:name] + # end + # end + # + # Customer.protected_attributes + # # => #<ActiveModel::MassAssignmentSecurity::BlackList: {:name}> def attributes_protected_by_default [] end + # Defines sanitize method. + # + # class Customer + # include ActiveModel::MassAssignmentSecurity + # + # attr_accessor :name + # + # attr_protected :name + # + # def assign_attributes(values) + # sanitize_for_mass_assignment(values).each do |k, v| + # send("#{k}=", v) + # end + # end + # end + # + # # See ActiveModel::MassAssignmentSecurity::StrictSanitizer for more information. + # Customer.mass_assignment_sanitizer = :strict + # + # customer = Customer.new + # customer.assign_attributes(name: 'David') + # # => ActiveModel::MassAssignmentSecurity::Error: Can't mass-assign protected attributes for Customer: name + # + # Also, you can specify your own sanitizer object. + # + # class CustomSanitizer < ActiveModel::MassAssignmentSecurity::Sanitizer + # def process_removed_attributes(klass, attrs) + # raise StandardError + # end + # end + # + # Customer.mass_assignment_sanitizer = CustomSanitizer.new + # + # customer = Customer.new + # customer.assign_attributes(name: 'David') + # # => StandardError: StandardError def mass_assignment_sanitizer=(value) self._mass_assignment_sanitizer = if value.is_a?(Symbol) const_get(:"#{value.to_s.camelize}Sanitizer").new(self) @@ -224,12 +339,12 @@ module ActiveModel protected - def sanitize_for_mass_assignment(attributes, role = :default) - _mass_assignment_sanitizer.sanitize(attributes, mass_assignment_authorizer(role)) + def sanitize_for_mass_assignment(attributes, role = nil) #:nodoc: + _mass_assignment_sanitizer.sanitize(self.class, attributes, mass_assignment_authorizer(role)) end - def mass_assignment_authorizer(role = :default) - self.class.active_authorizer[role] + def mass_assignment_authorizer(role) #:nodoc: + self.class.active_authorizer[role || :default] end end end |