mass_assignment_security.rb 8.5 KB
Newer Older
1 2
require 'active_support/core_ext/class/attribute.rb'
require 'active_model/mass_assignment_security/permission_set'
3
require 'active_model/mass_assignment_security/sanitizer'
4

5
module ActiveModel
6
  # = Active Model Mass-Assignment Security
7
  module MassAssignmentSecurity
8 9 10 11 12 13
    extend ActiveSupport::Concern

    included do
      class_attribute :_accessible_attributes
      class_attribute :_protected_attributes
      class_attribute :_active_authorizer
14 15 16 17 18 19 20

      class_attribute :mass_assignment_sanitizer, :mass_assignment_sanitizers
      self.mass_assignment_sanitizer = :logger
      self.mass_assignment_sanitizers = {
        :logger => LoggerSanitizer.new(self.respond_to?(:logger) && self.logger),
        :strict => StrictSanitizer.new
      }
21 22
    end

23 24 25 26 27 28 29 30
    # 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:
    #
31 32
    #   class AccountsController < ApplicationController
    #     include ActiveModel::MassAssignmentSecurity
33
    #
34
    #     attr_accessible :first_name, :last_name
35
    #     attr_accessible :first_name, :last_name, :plan_id, :as => :admin
36
    #
37 38 39 40 41
    #     def update
    #       ...
    #       @account.update_attributes(account_params)
    #       ...
    #     end
42
    #
43
    #     protected
44
    #
45
    #     def account_params
46 47
    #       role = admin ? :admin : :default
    #       sanitize_for_mass_assignment(params[:account], role)
48
    #     end
49
    #
50
    #   end
51
    #
52 53 54 55 56 57 58 59 60 61
    # = 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.
    #
    # 
62
    module ClassMethods
63
      # Attributes named in this macro are protected from mass-assignment
64 65 66
      # 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.
67 68 69 70
      #
      # 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
71
      # tampering with URLs or forms. Example:
72 73 74 75 76 77
      #
      #   class Customer
      #     include ActiveModel::MassAssignmentSecurity
      #
      #     attr_accessor :name, :credit_rating
      #
78 79 80 81 82
      #     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|
83 84 85
      #         send("#{k}=", v)
      #       end
      #     end
86 87
      #   end
      #
88
      # When using the :default role :
89
      #
90
      #   customer = Customer.new
91
      #   customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :default)
92
      #   customer.name          # => "David"
93
      #   customer.credit_rating # => nil
94
      #   customer.last_login    # => nil
95 96 97 98
      #
      #   customer.credit_rating = "Average"
      #   customer.credit_rating # => "Average"
      #
99
      # And using the :admin role :
100 101 102 103 104 105 106
      #
      #   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
      #
107 108 109 110 111
      # 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.
112 113
      def attr_protected(*args)
        options = args.extract_options!
114
        role = options[:as] || :default
115 116

        self._protected_attributes        = protected_attributes_configs.dup
117
        self._protected_attributes[role] = self.protected_attributes(role) + args
118

119 120
        self._active_authorizer = self._protected_attributes
      end
121

122
      # Specifies a white list of model attributes that can be set via
123
      # mass-assignment.
124
      #
125 126
      # 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
127 128
      # using the :as option.
      #
129 130 131 132 133 134 135 136
      # 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+.
      #
137 138 139 140
      #   class Customer
      #     include ActiveModel::MassAssignmentSecurity
      #
      #     attr_accessor :name, :credit_rating
141
      #
142
      #     attr_accessible :name
143
      #     attr_accessible :name, :credit_rating, :as => :admin
144
      #
145 146
      #     def assign_attributes(values, options = {})
      #       sanitize_for_mass_assignment(values, options[:as]).each do |k, v|
147 148 149
      #         send("#{k}=", v)
      #       end
      #     end
150 151
      #   end
      #
152
      # When using the :default role :
153
      #
154
      #   customer = Customer.new
155
      #   customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :default)
156
      #   customer.name          # => "David"
157 158 159 160 161
      #   customer.credit_rating # => nil
      #
      #   customer.credit_rating = "Average"
      #   customer.credit_rating # => "Average"
      #
162
      # And using the :admin role :
163 164 165 166 167 168
      #
      #   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"
      #
169 170
      # 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.
171 172
      def attr_accessible(*args)
        options = args.extract_options!
173
        role = options[:as] || :default
174 175

        self._accessible_attributes        = accessible_attributes_configs.dup
176
        self._accessible_attributes[role] = self.accessible_attributes(role) + args
177

178 179
        self._active_authorizer = self._accessible_attributes
      end
180

181 182
      def protected_attributes(role = :default)
        protected_attributes_configs[role]
183
      end
184

185 186
      def accessible_attributes(role = :default)
        accessible_attributes_configs[role]
187
      end
188

189 190
      def active_authorizers
        self._active_authorizer ||= protected_attributes_configs
191
      end
192
      alias active_authorizer active_authorizers
193

194 195
      def attributes_protected_by_default
        []
196
      end
197 198 199 200 201

      private

      def protected_attributes_configs
        self._protected_attributes ||= begin
202
          default_black_list = BlackList.new(attributes_protected_by_default)
203 204 205 206 207 208
          Hash.new(default_black_list)
        end
      end

      def accessible_attributes_configs
        self._accessible_attributes ||= begin
209
          default_white_list = WhiteList.new
210 211 212
          Hash.new(default_white_list)
        end
      end
213 214
    end

215
  protected
216

217
    def sanitize_for_mass_assignment(attributes, role = :default)
218 219 220 221 222 223 224
      sanitizer = case mass_assignment_sanitizer
                  when Symbol
                    self.mass_assignment_sanitizers[mass_assignment_sanitizer]
                  else
                    mass_assignment_sanitizer
                  end
      sanitizer.sanitize(attributes, mass_assignment_authorizer(role))
225
    end
226

227 228
    def mass_assignment_authorizer(role = :default)
      self.class.active_authorizer[role]
229
    end
230 231
  end
end