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