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
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.
When I add the user user
to the farm Access denied
, such as ...
create(:farm, user: user, name: "Access denied")
... then the test succeeds.
get "#{url}.json"
actually consider the status of the user? Is this all done by load_and_authorize_resource
in the FarmsController
?@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?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:
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.
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
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
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