Question

I am trying to test the user authentication for the Farm model, in this case for the :user role which has read access to all farms when being logged-in (as the guest user aka. anonymous has too).

# /models/ability.rb
class Ability
  include CanCan::Ability

  def initialize(user)
    # Create guest user aka. anonymous (not logged in) when user is nil.
    user ||= User.new

    if user.has_role? :admin
      can :manage, :all
    else # guest user aka. anonymous
      can :read, :all
      # logged in user
      if user.has_role? :user
        can :create, Farm
        can :manage, Farm, :user_id => user.id
      end
    end

  end
end

...

# /controllers/api/v1/farms_controller.rb
class Api::V1::FarmsController < ActionController::Base

    load_and_authorize_resource
    rescue_from CanCan::AccessDenied do |exception|
        redirect_to farms_path, alert: exception.message
    end
    respond_to :json

    def index
        # Next line might be redundant refering to the CanCan wiki. See below..
        @farms = Farm.accessible_by(current_ability, :read)
        respond_with(@farms)
    end
end

...

# /spec/api/v1/farm_spec.rb
require "spec_helper"

describe "/api/v1/farms" do
    let(:user) { create(:user) } # lets call this user1 in the discussion
    let(:token) { user.authentication_token }

    before do
        user.add_role :user
        create(:farm, user: user, name: "Testfarm")
        create(:farm, name: "Access denied")
        @ability = Ability.new(user)
    end

    context "farms viewable by this logged-in user" do
        let(:url) { "/api/v1/farms" }
        it "json" do
            get "#{url}.json"

            farms_json = Farm.accessible_by(@ability, :read).to_json

            assert last_response.ok?
            last_response.body.should eql(farms_json)
            last_response.status.should eql(200)

            farms = JSON.parse(last_response.body)

            farms.any? do |farm|
                farm["name"] == "Testfarm"
            end.should be_true

            farms.any? do |farm|
                farm["name"] == "Access denied"
            end.should be_true

        end
    end
end

The problem

When I inspect farms_json I can see it contains only the Testfarm. When I inspect the last_response I can see it contains both the Testfarm and Access denied. This is strange since I use the same accessible_by method both in the spec and the index action. The setup I use is described in the wiki of the CanCan gem entitled Fetching Records.

The useless workaround

When I add the user user to the farm Access denied, such as ...

create(:farm, user: user, name: "Access denied")

... then the test succeeds.

The questions

  1. Why is the "Access denied" farm not returned although it can be read by any user (including guest users)?
  2. Does get "#{url}.json" actually consider the status of the user? Is this all done by load_and_authorize_resource in the FarmsController?
  3. The wiki mentions that @farms = Farm.accessible_by(current_ability, :read) can be left out since "this is done automatically by load_resource for the index action". Does this apply to my situation?

Experiments

I created another user "user2" and another farm "My little farm". I linked those to each other. This way the database in the example contains three farms alltogether:

  • Farm "Testfarm" associated to user1
  • Farm "Access denied" associated to no user
  • Farm "My little farm" associated to user2.

When I run Farm.accessible_by(Ability.new(user1), :read) I still only receive "Testfarm".

Was it helpful?

Solution

The answer to my question consists of multiple parts. I hope this clarifies the setup to everyone else who deals with a similar configuration.

1. Ability Precedence

First of all please mind that the order of ability rules does matter as described in Ability Precedence. After realizing this fact I came up with an updated set of ability rules.

# /models/ability.rb
class Ability
  include CanCan::Ability

  def initialize(user)
    # Create guest user aka. anonymous (not logged-in) when user is nil.
    user ||= User.new

    if user.has_role? :admin
      can :manage, :all
    else
      # logged in user
      if user.has_role? :user
        can :manage, Farm, :user_id => user.id
        can :create, Farm
      end
       # guest user aka. anonymous
      can :read, :all
    end
  end
end

2. FarmsContoller

Keep it simple in the index action. load_and_authorize_resource is your friend.

# /controllers/api/v1/farms_controller.rb
class Api::V1::FarmsController < ActionController::Base

    load_and_authorize_resource
    rescue_from CanCan::AccessDenied do |exception|
        redirect_to farms_path, alert: exception.message
    end
    respond_to :json

    def index
        respond_with(@farms)
    end
end

3. Get request with authentication token

Do not forget to pass the token when you request data from the farms controller.

# # /spec/api/v1/farm_spec.rb
get "#{url}.json", auth_token: :token

The token must be added in the User model as follows.

# app/models/user.rb
class User < ActiveRecord::Base
    before_save :ensure_authentication_token

And the name of the method can be configured in the initializer of Devise.

# config/initializers/devise.rb
config.token_authentication_key = :auth_token
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top