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)

# somewhere in the controller

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:

module ActiveModel
  # Raised when forbidden attributes are used for mass assignment.
  #   class Person < ActiveRecord::Base
  #   end
  #   params = 'Bob')
  #   # => ActiveModel::ForbiddenAttributesError
  #   params.permit!
  #   # => #<Person id: nil, name: "Bob">
  class ForbiddenAttributesError < StandardError

  module ForbiddenAttributesProtection # :nodoc:
      def sanitize_for_mass_assignment(attributes)
        if attributes.respond_to?(:permitted?) && !attributes.permitted?
          raise ActiveModel::ForbiddenAttributesError
      alias :sanitize_forbidden_attributes :sanitize_for_mass_assignment

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.