Context aware authorization using CanCan
Ok, I solved the problem. My use case is briefly mentioned in the beginning of the CanCan README and I missed it. You can define new Ability classes in app/models/ that take in a different parameter other than current_user. To do so, you put the following in your controller:
def current_ability
if params[:controller] == 'leagues'
@current_ability = LeagueAbility.new(current_user_league_relation)
elsif params[:controller] == 'league_relations'
@current_ability = LeagueRelationAbility.new(current_user_league_relation)
else
@current_ability = Ability.new(current_user)
end
end
Now you can create league_ability.rb in app/models/.
class LeagueAbility
include CanCan::Ability
def initialize(league_relation)
league_relation ||= LeagueRelation.new
if league_relation.owner?
can :manage, League, :id => league_relation.league_id
elsif league_relation.moderator?
can :manage, League, :id => league_relation.league_id
cannot [:delete, :destroy], League
else
can :read, League
can :create, League
end
end
end
One thing to note is that this relies on your application controller calling a method in a child class. Hope that helps!
CanCan: load_and_authorize_resource in namespace other than that of MainApp
It seems to be a bug in CanCan::ControllerResource#namespace
:
def namespace
@params[:controller].split("::")[0..-2]
end
As you see, it tries to split controller path by ::
but it comes in the form of my_engine/my_controller
.
So the fix is dumb simple:
def namespace
@params[:controller].split("/")[0..-2]
end
Wonder how they could miss such a stupid bug for so long. Shall send them a pull request.
P.S. Have just signed up to answer 8)
Is it possible to raise an exception other than Access Denied with cancan?
I did some digging and it doesn't look like CanCan supports the kind of granularity for exceptions you require.
So... why not use current_user.status
in your controller as well?
def index
authorize! :show, Organization
rescue CanCan::AccessDenied => e
if current_user.status
render_403
else
render_404
end
end
(Or, if duplicate code/logic is a concern, put them both in a method you call both in your ability class and the controller.)
Then simplify the snippet from your ability class down to the following:
can :show, Organization
BTW, CanCan hasn't been maintained in years. You may want to switch to the more actively maintained CanCanCan (but I think you'll require the same solution above).
Also, if you end up showing different pages depending on the user's status (whatever that status may communicate), you may be inadvertently leaking information that users and/or non-users aren't supposed to see, similar to what's explained here.
Cancan authorization in Non RESTful controller
I tried to debug following the link Debugging Abilities and figured out what was wrong.
The abilities were correct, I was doing a small mistake by specifying them in the wrong order.
For others coming on to this page, if you face any problems with Cancan abilities not working, try to debug using the above link. You will eventually figure out whats not working and why.
Defining abilities in more complex environment with role and group models
I had the same problem just the other day. I figured out the solution after reading the CanCan readme, which you should do if you haven't yet.
You can view my solution here: Context aware authorization using CanCan
To give you an answer more specific to your use case, do the follow:
In your application controller you need to define some logic which will pick your abilities.
class ApplicationController < ActionController::Base
check_authorization
def current_ability
if <no group selected logic> # Maybe: params[:controller] == 'groups'
@current_ability = NoGroupSelectedAbility.new(current_user)
else
@current_ability = GroupSelectedAbility.new(current_user)
end
end
# Application Controller Logic Below
end
You'll then need to create a new ability (or abilities) in your app/models/ folder. You can also do cool stuff like this:
if request.path_parameters[:controller] == groups
@current_ability = GroupsAbility.new(current_group_relation)
end
Where current_group_relation is defined in app/controllers/groups_controller.rb. This will give you specific abilities for specific controllers. Remember that a parent classes can call methods in child classes in Ruby. You can define a method in your controller, and call it from ApplicationController, as long as you are certain what controller is currently being used to handle the request.
Hope that helps.
EDIT: I wanted to show you what a custom ability looks like.
# File path: app/models/group_ability.rb
class GroupsAbility
include CanCan::Ability
# This can take in whatever you want, you decide what to argument to
# use in your Application Controller
def initialize(group_relation)
group_relation ||= GroupRelation.new
if group_relation.id.nil?
# User does not have a relation to a group
can :read, all
elsif group_relation.moderator?
# Allow a mod to manage all group relations associated with the mod's group.
can :manage, :all, :id => group_relation.group.id
end
end
end
How to modify cancan load and authorize resource to load resource using different id
The resource will only be loaded into an instance variable if it hasn't been already. This allows you to easily override how the loading happens in a separate before_filter.
class EventsController < ApplicationController
before_filter :find_event
load_and_authorize_resource
private
def find_event
@event = Event.find(params[:event_id])
end
end
Read more in the docs: https://github.com/CanCanCommunity/cancancan/wiki/Authorizing-controller-actions
Should I skip authorization, with CanCan, of an action that instantiates a resource?
I found my problem. CanCan was not the issue at all! The following line in my CardSet controller was throwing the exception and redirecting my guest (not logged in) users to the log in page:
before_filter :authenticate_user!, :except => [:show, :index]
I changed it to read:
before_filter :authenticate_user!, :except => [:show, :index, :random]
And now the code works as intended: Guest users can view the new random sets created, but cannot 'Save' them unless they first log in.
So, my real problem was with Devise (or, actually, my Devise configuration) and not with CanCan at all.
Related Topics
How to Use Active Record Without Rails
Rails: Creating a Custom Data Type/Creating a Shorthand
How to Apply a Patch to Ruby on Rails
How to Use Ruby Minitest::Spec with Rails for API Integration Tests
Post JSON Data to Simple Rails Application with Curl
Paginate Multiple Models in Kaminari
How Does Rails Implement Before_Filter
Understanding Io.Select When Reading Socket in Ruby
Ruby on Rails and JSON Parser from Url
How to Validate Ssl Certificate Chain in Ruby with Net/Http
Rails 4.2 - Sidekiq Not Sending Emails in Development
Which Ruby Gems Support the Facebook API
Rails, Ruby, How to Sort an Array
Best Way to Generate Order Numbers for an Online Store
Rails Collection_Select VS. Select
Object.Valid? Returns False But Object.Errors.Full_Messages Is Empty