Rails #permitted
I recently upgraded a personal app I use for learning new things in Rails. But when I upgraded from 4.1.4 to 4.1.12 I encountered this familiar error.
Customer.where(auth.slice(:provider, :uid)).first_or_initialize
ActiveModel::ForbiddenAttributesError: ActiveModel::ForbiddenAttributesError
from /Users/greg/.rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/activemodel-4.1.12/lib/active_model/forbidden_attributes_protection.rb:21:in `sanitize_for_mass_assignment`
Now I remember. It’s one of those mass assignments where you have to specify the ‘permitted’ values before you can continue. In Rails 4, it’s good practice to whitelist the attributes you receive in the controller and it goes something like this:
def user_params
params.require(:user).permit(:username, :email, :password)
end
# somewhere in the controller
Customer.create(user_params)
Now, let’s use this idiom. It should be easy, right?
> auth.permit(:provider, :uid)
=> nil
Wait, that didn’t go as expected. How about just simply composing the hash?
> Customer.where(provider: auth[:provider], uid: auth[:uid]).first_or_initialize
=> #<Customer:0x007fd9168ffa88>
Interesting. #permit returns nil, using plain hash works, and #slice doesn’t.
> auth.slice(:provider, :uid).class
=> OmniAuth::AuthHash < Hashie::Mash
It shouldn’t matter what auth is as long as it behaves like what the Customer model expects. But what does the Customer model expects? Actually, the error message is telling us what it expects. In Rails, there is this module for mass assignment protection:
# https://github.com/rails/rails/blob/master/activemodel/lib/active_model/forbidden_attributes_protection.rb
module ActiveModel
# Raised when forbidden attributes are used for mass assignment.
#
# class Person < ActiveRecord::Base
# end
#
# params = ActionController::Parameters.new(name: 'Bob')
# Person.new(params)
# # => ActiveModel::ForbiddenAttributesError
#
# params.permit!
# Person.new(params)
# # => #<Person id: nil, name: "Bob">
class ForbiddenAttributesError < StandardError
end
module ForbiddenAttributesProtection # :nodoc:
protected
def sanitize_for_mass_assignment(attributes)
if attributes.respond_to?(:permitted?) && !attributes.permitted?
raise ActiveModel::ForbiddenAttributesError
else
attributes
end
end
alias :sanitize_forbidden_attributes :sanitize_for_mass_assignment
end
end
Nothing fancy here. Rails does a simple check whether to allow mass assignment or not.
> auth.slice(:provider, :uid).permitted?
=> false
> { provider: auth[:provider], uid: auth[:uid] }.permitted?
NoMethodError: undefined method `permitted?' for {:provider=>"facebook", :uid=>"123"}:Hash
OmniAuth::AuthHash does not even allow it. Plain Hash works because it doesn’t even respond to #permitted.