Search code examples
ruby-on-railsclearance

ForbiddenAttributesError With Clearance


I am using clearance for signup and have a couple extra attributes beyond email and password that are in the signup form including first_name, last_name, role, and university conditional on if role is staff.

I have three user roles:

  enum role: { staff: 0, clinician: 1, admin: 2 }

Something is wrong with my implementation, because signup attempt leads to the following error:

ActiveModel::ForbiddenAttributesError in Clearance::UsersController#create

with this line of code highlighted as the offending line:

 Clearance.configuration.user_model.new(user_params).tap do |user|

What am I doing wrong? Any help would be greatly appreciated.

Here is my entire app/clearance/users_controller.rb

class Clearance::UsersController < Clearance::BaseController
  if respond_to?(:before_action)
    before_action :redirect_signed_in_users, only: [:create, :new]
    skip_before_action :require_login, only: [:create, :new], raise: false
    skip_before_action :authorize, only: [:create, :new], raise: false
  else
    before_filter :redirect_signed_in_users, only: [:create, :new]
    skip_before_filter :require_login, only: [:create, :new], raise: false
    skip_before_filter :authorize, only: [:create, :new], raise: false
  end
  layout 'authentication'

  def new
    @user = user_from_params
    render template: "users/new"
  end

  def create
    @user = user_from_params

    if @user.save
      case @user.role
      when "staff"
        validate_user(@user)
      when "clinician"
        user.update_attribute(:approved, true)
        deliver_email(@user)
      end
    else
      render template: "users/new"
    end
  end

  private

  def validate_user(user)
    if user.whitelisted?
      user.update_attribute(:approved, true)
      deliver_email(user)
    else
      redirect_to sign_in_path,
        notice: "Your request will be analyzed"
    end
  end

  def deliver_email(user)
    user.forgot_password! #Generates confirmation token only
    mail = ::ClearanceMailer.confirm_email(user)

    if mail.respond_to?(:deliver_later)
      mail.deliver_later
    else
      mail.deliver
    end
    redirect_to sign_in_path,
      notice: "Please confirm your email address."
  end

  def avoid_sign_in
    warn "[DEPRECATION] Clearance's `avoid_sign_in` before_filter is " +
      "deprecated. Use `redirect_signed_in_users` instead. " +
      "Be sure to update any instances of `skip_before_filter :avoid_sign_in`" +
      " or `skip_before_action :avoid_sign_in` as well"
    redirect_signed_in_users
  end

  def redirect_signed_in_users
    if signed_in?
      redirect_to Clearance.configuration.redirect_url
    end
  end

  def url_after_create
    Clearance.configuration.redirect_url
  end

def user_params
  params.require(:user).permit(
    :email, 
    :password, 
    :role, 
    :first_name, 
    :last_name, 
    :university_id
  )
end

    Clearance.configuration.user_model.new(user_params).tap do |user|
      user.email = email
      user.password = password
      user.role = role
      user.first_name = first_name
      user.last_name = last_name
      if role == "staff"
        user.university = University.find(university)
      end
    end
  end

  def user_params
    params[Clearance.configuration.user_parameter] || Hash.new
  end
end

Solution

  • Clearance is built to be usable with new versions of Rails which use Strong Parameters and older versions of Rails that do not. This means this code is a bit more complex than desired. The default implementation of user_from_params explicitly assigns email and password so we completely avoid mass assignment when using attr_accessible or strong parameters. However, we also pass the remaining parameters through to the user model with Clearance.configuration.user_model.new(user_params).

    Instead of overriding user_from_params, I'd investigate overriding user_params instead, to explicitly permit the fields you want to allow through:

    def user_params
      params.require(:user).permit(
        :email, 
        :password, 
        :role, 
        :first_name, 
        :last_name, 
        :university_id
      )
    end
    

    It's worth pointing out that assigning role like this does actually seem like a mass assignment vulnerability. You're allowing the user to select their access level. If this is some sort of trusted system where this is okay, then the approach you're taking seems fine I guess. If you're relying on some front end form fields to limit who can register for each role... don't.

    This method should live inside your users controller, which should inherit from Clearance::UsersController. It seems you've opted to redefine Clearance::UsersController instead. I'd recommend creating your own UsersController like the following:

    class UsersController < Clearance::UsersController
      layout "authentication"
    
      private
    
      def user_params
        params.require(:user).permit(
          :email, 
          :password, 
          :role, 
          :first_name, 
          :last_name, 
          :university_id
        )
      end
    end
    

    This has none of the user validation logic of the controller you submitted. I'd try to move this to your User model. Failing that, you can override create in your controller as well.

    You'll need to make sure your routes file points to your UsersController rather than Clearance::UsersController.