3
votes

I'm trying to get cancan incorporated into my first ever Ruby on Rails app.

I'm having a problem getting started... its surely something basic.

My application has a list of projects, and a user may or may not have permission to see any number of them.

I added this to my ProjectsController:

class ProjectsController < ApplicationController
  load_and_authorize_resource

My initialize method looks like this:

  def initialize(user)
    user ||= User.new # guest user
    puts "********  Evaluating cancan permissions for: " + user.inspect
    can :read, Project do |project|
      puts "********  Evaluating project permissions for: " + project.inspect
      # project.try(project_users).any?{|project_user| project_user.user == user} 
      1 == 1  #POC test!
    end
  end

When I have this, the project index page appears, but no projects are listed.

2 questions I have here:

  1. Shouldn't all of the projects appear since true is returned for all projects?
  2. The second puts statement is not written to the rails server console, but the first one is. Why is that???

If I change the initialize method to:

  def initialize(user)
    user ||= User.new # guest user
    puts "********  Evaluating cancan permissions for: " + user.inspect
    can :read, Project
  end

... I see all of the projects as I would expect

If I remove the can :read, Project line, I get a security exception trying to hit the projects index page.... also what I'd expect.

1

1 Answers

0
votes

The block being passed to the :read ability is only evaluated when an instance of the project is available (@project). Because you are talking about the index action, only the collection is available (@projects). This explains why your second puts statement is never appearing. In order to limit your index actions, you need to either pass a hash of conditions into the can method, or use a scope (in addition to the block). All of this information is clearly outlined in the CanCan wiki on Github.

So the puts problem is explainable. What does not make sense is how no projects are showing. When evaluating the index action, CanCan will actually default to ignoring the block entirely. This means that your ability is essentialy can :read, Project anyway (even in the first example) for the index action.

I would be interested to have you try to add a simple scope, just to see if it will work. Try:

can :read, Project, Project.scoped do |project|
  true
end

And then see what happens for the index action.

edit:

Given that you can see the projects in the index now, it seems like you need to pass a scope into the ability as well as a block. Please read this Github issue where Ryan explains why the block is not evaluated on the index action.

Blocks are only intended to be used for defining abilities based on an object's attributes. [...] That is the only case when a block should be used because the block is only executed when an object is available. All other conditions should be defined outside the block.

Keep in mind that if your ability is not too complex for a hash of conditions, you should use that instead. The hash of conditions is explained on this CanCan wiki page on Github. If you do need a scope, you will need to pass in the scope and the block. Lets say you have the ability shown above.

  1. On the index action, CanCan will disregard the block because a Project object (@project) is not available. It will instead return projects that are within the scope given, in this case Project.scoped (which will just be all projects).
  2. On the show action, @project is available, so CanCan will evaluate the block and allow the action if the block evaluates to true.

So the reason you need to pass both is so that CanCan can handle both the index and show actions. In most cases, your block will define the same thing as the scope does, only the block will be written in Ruby while your scope will be written Rails' ActiveRecord syntax. You can fine more information about here: Defining Abilities with Blocks.