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
- Why is the “Access denied” farm not returned although it can be read by any user (including guest users)?
- Does
get "#{url}.json"actually consider the status of the user? Is this all done byload_and_authorize_resourcein theFarmsController? - The wiki mentions that
@farms = Farm.accessible_by(current_ability, :read)can be left out since “this is done automatically byload_resourcefor 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”.
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.
2. FarmsContoller
Keep it simple in the index action.
load_and_authorize_resourceis your friend.3. Get request with authentication token
Do not forget to pass the token when you request data from the farms controller.
The token must be added in the User model as follows.
And the name of the method can be configured in the initializer of Devise.