Search code examples
jqueryruby-on-rails-3cancan

jquery ui autocomplete, rails3 and CanCan model access problem


I have various roles defined by CanCan in my rails application. I recently implemented jQuery UI autocomplete and it works well. The problem is that when I submit the form, the find_by_name that occurs in the model can find records that do not belong to the current_user. I have the following in my view:

<strong><%= f.label :inventory_name, "Material" %></strong>
<%= f.text_field :inventory_name, :class => "inputbox" %><br>

And my jQuery looks like:

jQuery("input[id$=_inventory_name]").autocomplete({
  source: '/ajax/inventory',
  minLength: 2
});

Then I have an ajax controller that does the right thing:

def inventory
  inventory = Inventory.accessible_by(current_ability)
  if params[:term]
    like= "%".concat(params[:term].concat("%"))
    names = inventory.where("name LIKE ?", like)
  else
    names = inventory
  end

  list = names.map {|u| Hash[ :id => u.id, :label => u.name, :name => u.name]}
  render :json => list
end

But my model does not:

def inventory_name=(name)
  inventory = Inventory.find_by_name(name)
  if inventory
    self.inventory_id = inventory.id
  else
    errors[:inventory_name] << "Invalid name entered"
  end
end
def inventory_name
  Inventory.find(inventory_id).name if inventory_id
end

find_by_name will return the first match it finds regardless of who owns it. Ideally I'd like to change:

inventory = Inventory.find_by_name(name)

to

inventory = Inventory.accessible_by(current_ability).find_by_name(name)

But that violates the principles of MVC not to mention the model has no access to current_ability, current_user or the like. So my question is, how to I move this logic into my controller where I have access to these things? I can't seem to wrap my head around it :(


Solution

  • I ended up needing both a before and after filter:

    after_filter :save_new_project_inventory, :only => [:create]
    before_filter :update_project_inventory, :only => [:create, :update]
    

    ...

    private
    def save_new_project_inventory
      # do this in an after filter so that
      # we will have a project.id
      @project_inventories.each { |key, value|
        value[:project_id] = @project.id
        ProjectInventory.create!(value)
      }
    end
    
    def update_project_inventory
      @project_inventories = {}
      params[:project][:project_inventories_attributes].each { |key, value|
        if value[:_destroy] != "false"
          project_inventory = ProjectInventory.find(value[:id])
          project_inventory.delete
        else
          if value[:id]
            project_inventory = ProjectInventory.find(value[:id])
            project_inventory.inventory_id = inventory_name(value[:inventory_name])
            project_inventory.save
          else
            if @project.nil?
              # can't save here because we need a project.id that we
              # won't have until after we save, so finish up in the
              # after filter
              @project_inventories[key] = {
                :inventory_id => inventory_name(value[:inventory_name])
              }
            else
              project_inventory = ProjectInventory.new
              project_inventory.inventory_id = inventory_name(value[:inventory_name])
              project_inventory.project_id = params[:id]
              project_inventory.save
            end
          end
        end
      }
      params[:project].delete(:project_inventories_attributes)
    end
    
    def inventory_name(name)
      inventories = Inventory.accessible_by(current_ability)
      inventory = inventories.find_by_name(name)
      if inventory
        inventory.id.to_s
      end
    end
    

    I'm not sure if it is the best way to do it, but it works.