Search code examples
ruby-on-rails-3.2formtastic

Create method for nested resource with protected ID attribute


I’m following the (now slightly outdated) Meet Rails 3 tutorial from PeepCode, and am having trouble getting a couple of the tutorial’s suggestions to work together with Rails 3.2.

The tutorial has you create a Role model that belongs to a Project:

class Role < ActiveRecord::Base
  belongs_to :project
  validates :project_id, :presence => true
  attr_protected :project_id
end

The routes.rb file nests Role resources such that you must work with a Role in the context of a Project:

resources :projects do
  resources :roles
end

Note in the model code above, the tutorial advises you to use attr_protected to protect the :project_id field, because it can be set “more securely” by creating every Role in the context of a project, like this in roles_controller.rb:

class RolesController < ApplicationController
  ⋮

  def create
    @role = project.roles.new(params[:role])
    ⋮

The problem is, the HTML form for creating a Role, which is created with Formtastic, contains a project_id field for selecting the project. Therefore, when project.roles.new(params[:role]) tries to use the parameters from the form to populate the new Role object, it tries to set the project_id using mass assignment, and fails with:

ActiveModel::MassAssignmentSecurity::Error in RolesController#create
Can’t mass-assign protected attributes: project_id

What is the accepted way to implement this? Was protecting the project_id attribute a bad idea? Or is there some way to populate the new Role with the form data without including project_id?


Solution

  • If you are getting project via params[:project_id] rather than params[:role][:project_id] you could actually be setting conflicting values anyway.

    The reason Mass Assignment would want to protect this is to prevent a user entering in an arbitrary value for project_id that could allow a project that isn't under this users control. You have a couple of options.

    If you had an authorative user or account attached to the object you could add in a before_save callback, such as self.project_id = nil unless user.projects.find(project_id).

    Since you don’t, I'd use the project_id from the hash to find the project, and fall back to the route id (I'm not sure if it would be project_id or just id off the top of my head).

    def create
      user.
        projects.
        find(params[:role].delete(:project_id) || params[:project_id] || params[:id]).
        create(params[:role])
    

    The easiest thing would be to just drop the select box from the form, since they've selected a project when choosing to create a new role – it's a nested resource.