aboutsummaryrefslogtreecommitdiffstats
path: root/activemodel/lib/active_model/mass_assignment_security.rb
blob: 95de039676e33c5ba42e376e1d0bf588d86da69a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
require 'active_support/core_ext/class/attribute'
require 'active_support/core_ext/string/inflections'
require 'active_model/mass_assignment_security/permission_set'
require 'active_model/mass_assignment_security/sanitizer'

module ActiveModel
  # = Active Model Mass-Assignment Security
  module MassAssignmentSecurity
    extend ActiveSupport::Concern

    included do
      extend ActiveModel::Configuration

      config_attribute :_accessible_attributes
      config_attribute :_protected_attributes
      config_attribute :_active_authorizer

      config_attribute :_mass_assignment_sanitizer
      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.
      #
      # 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:
      #
      #   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 <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of +attr_protected+
      # to sanitize attributes won't provide sufficient protection.
      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._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.
      #
      # 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
      #
      #     attr_accessible :name
      #     attr_accessible :name, :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 <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of +attr_accessible+
      # to sanitize attributes won't provide sufficient protection.
      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._active_authorizer = self._accessible_attributes
      end

      def protected_attributes(role = :default)
        protected_attributes_configs[role]
      end

      def accessible_attributes(role = :default)
        accessible_attributes_configs[role]
      end

      def active_authorizers
        self._active_authorizer ||= protected_attributes_configs
      end
      alias active_authorizer active_authorizers

      def attributes_protected_by_default
        []
      end

      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)
      _mass_assignment_sanitizer.sanitize(attributes, mass_assignment_authorizer(role))
    end

    def mass_assignment_authorizer(role)
      self.class.active_authorizer[role || :default]
    end
  end
end