mass_assignment_security.rb 8.5 KB
Newer Older
D
Damien Mathieu 已提交
1
require 'active_support/core_ext/class/attribute'
2
require 'active_support/core_ext/string/inflections'
3
require 'active_model/mass_assignment_security/permission_set'
4
require 'active_model/mass_assignment_security/sanitizer'
5

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

    included do
12
      extend ActiveModel::Configuration
13

14 15 16 17 18
      config_attribute :_accessible_attributes
      config_attribute :_protected_attributes
      config_attribute :_active_authorizer

      config_attribute :_mass_assignment_sanitizer
19
      self.mass_assignment_sanitizer = :logger
20 21
    end

22 23 24 25 26 27 28 29
    # 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:
    #
30 31
    #   class AccountsController < ApplicationController
    #     include ActiveModel::MassAssignmentSecurity
32
    #
33
    #     attr_accessible :first_name, :last_name
34
    #     attr_accessible :first_name, :last_name, :plan_id, :as => :admin
35
    #
36 37 38 39 40
    #     def update
    #       ...
    #       @account.update_attributes(account_params)
    #       ...
    #     end
41
    #
42
    #     protected
43
    #
44
    #     def account_params
45 46
    #       role = admin ? :admin : :default
    #       sanitize_for_mass_assignment(params[:account], role)
47
    #     end
48
    #
49
    #   end
50
    #
51 52 53 54 55 56 57 58 59
    # = 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.
    #
60
    #
61
    module ClassMethods
62
      # Attributes named in this macro are protected from mass-assignment
63 64 65
      # 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.
66 67 68 69
      #
      # 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
70
      # tampering with URLs or forms. Example:
71 72 73
      #
      #   class Customer
      #     include ActiveModel::MassAssignmentSecurity
74
      #
75
      #     attr_accessor :name, :email, :logins_count
76
      #
77
      #     attr_protected :logins_count
78
      #     # Suppose that admin can not change email for customer
79 80
      #     attr_protected :logins_count, :email, :as => :admin
      #
81 82
      #     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", "email" => "a@b.com", :logins_count => 5 }, :as => :default)
92
      #   customer.name          # => "David"
93
      #   customer.email # => "a@b.com"
94
      #   customer.logins_count    # => nil
95
      #
96
      # And using the :admin role:
97 98
      #
      #   customer = Customer.new
99
      #   customer.assign_attributes({ "name" => "David", "email" => "a@b.com", :logins_count => 5}, :as => :admin)
100
      #   customer.name          # => "David"
101
      #   customer.email # => nil
102
      #   customer.logins_count    # => nil
103
      #
104 105
      #   customer.email = "c@d.com"
      #   customer.email # => "c@d.com"
106
      #
107 108 109
      # To start from an all-closed default and enable attributes as needed,
      # have a look at +attr_accessible+.
      #
110 111 112
      # 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.
113 114
      def attr_protected(*args)
        options = args.extract_options!
115
        role = options[:as] || :default
116

A
Alexander Uvarov 已提交
117
        self._protected_attributes = protected_attributes_configs.dup
118

A
Aaron Patterson 已提交
119
        Array(role).each do |name|
120 121
          self._protected_attributes[name] = self.protected_attributes(name) + args
        end
122

123 124
        self._active_authorizer = self._protected_attributes
      end
125

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

A
Alexander Uvarov 已提交
180
        self._accessible_attributes = accessible_attributes_configs.dup
181

A
Aaron Patterson 已提交
182
        Array(role).each do |name|
183 184
          self._accessible_attributes[name] = self.accessible_attributes(name) + args
        end
185

186 187
        self._active_authorizer = self._accessible_attributes
      end
188

189 190
      def protected_attributes(role = :default)
        protected_attributes_configs[role]
191
      end
192

193 194
      def accessible_attributes(role = :default)
        accessible_attributes_configs[role]
195
      end
196

197 198
      def active_authorizers
        self._active_authorizer ||= protected_attributes_configs
199
      end
200
      alias active_authorizer active_authorizers
201

202 203
      def attributes_protected_by_default
        []
204
      end
205

206 207 208 209 210 211 212 213
      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

214 215 216 217
      private

      def protected_attributes_configs
        self._protected_attributes ||= begin
218
          Hash.new { |h,k| h[k] = BlackList.new(attributes_protected_by_default) }
219 220 221 222 223
        end
      end

      def accessible_attributes_configs
        self._accessible_attributes ||= begin
224
          Hash.new { |h,k| h[k] = WhiteList.new }
225 226
        end
      end
227 228
    end

229
  protected
230

231
    def sanitize_for_mass_assignment(attributes, role = nil)
232
      _mass_assignment_sanitizer.sanitize(attributes, mass_assignment_authorizer(role))
233
    end
234

235 236
    def mass_assignment_authorizer(role)
      self.class.active_authorizer[role || :default]
237
    end
238 239
  end
end