require 'active_support/concern' require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/string/inflections' require 'active_model' require 'active_model/mass_assignment_security/permission_set' require 'active_model/mass_assignment_security/sanitizer' module ActiveModel # == 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 # # * mass_assignment_sanitizer - Defines sanitize method. Possible # values are: # * :logger (default) - writes filtered attributes to logger # * :strict - raise ActiveModel::MassAssignmentSecurity::Error # on any protected attribute update. # # You can specify your own sanitizer object eg. MySanitizer.new. # See ActiveModel::MassAssignmentSecurity::LoggerSanitizer for # example implementation. module MassAssignmentSecurity extend ActiveSupport::Concern include ActiveModel::ForbiddenAttributesProtection included do class_attribute :_accessible_attributes, instance_writer: false class_attribute :_protected_attributes, instance_writer: false class_attribute :_active_authorizer, instance_writer: false class_attribute :_uses_mass_assignment_security, instance_writer: false self._uses_mass_assignment_security = false class_attribute :_mass_assignment_sanitizer, instance_writer: false self.mass_assignment_sanitizer = :logger end 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 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. # # class Customer # include ActiveModel::MassAssignmentSecurity # # attr_accessor :name, :email, :logins_count # # 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| # send("#{k}=", v) # end # end # end # # When using the :default role: # # customer = Customer.new # 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: # # customer = Customer.new # 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 Hash#except or Hash#slice 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(role).each do |name| self._protected_attributes[name] = self.protected_attributes(name) + args end self._uses_mass_assignment_security = true 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 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 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 # attributes you can use direct writer methods. This is meant to protect # sensitive attributes from being overwritten by malicious users # tampering with URLs or forms. If you'd rather start from an all-open # default and restrict attributes as needed, have a look at # +attr_protected+. # # class Customer # include ActiveModel::MassAssignmentSecurity # # attr_accessor :name, :credit_rating # # # 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| # send("#{k}=", v) # end # end # end # # When using the :default 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.credit_rating = 'Average' # customer.credit_rating # => "Average" # # And using the :admin 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" # # Note that using Hash#except or Hash#slice 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(role).each do |name| self._accessible_attributes[name] = self.accessible_attributes(name) + args end self._uses_mass_assignment_security = true self._active_authorizer = self._accessible_attributes end # Returns an instance of ActiveModel::MassAssignmentSecurity::BlackList # with the attributes protected by #attr_protected method. If no +role+ # is provided, then :default 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 # # => # # # Customer.protected_attributes(:default) # # => # # # Customer.protected_attributes(:admin) # # => # def protected_attributes(role = :default) protected_attributes_configs[role] end # Returns an instance of ActiveModel::MassAssignmentSecurity::WhiteList # with the attributes protected by #attr_accessible method. If no +role+ # is provided, then :default 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 # # => # # # Customer.accessible_attributes(:default) # # => # # # Customer.accessible_attributes(:admin) # # => # 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=> #, # # :default=># # # } 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 # # => # 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) else value end end private def protected_attributes_configs self._protected_attributes ||= begin Hash.new { |h,k| h[k] = BlackList.new(attributes_protected_by_default) } end end def accessible_attributes_configs self._accessible_attributes ||= begin Hash.new { |h,k| h[k] = WhiteList.new } end end end protected def sanitize_for_mass_assignment(attributes, role = nil) #:nodoc: unless _uses_mass_assignment_security sanitize_forbidden_attributes(attributes) end _mass_assignment_sanitizer.sanitize(self.class, attributes, mass_assignment_authorizer(role)) end def mass_assignment_authorizer(role) #:nodoc: self.class.active_authorizer[role || :default] end end end