In the CanCanCan Documentation, it shows what the per-action equivalent of load_and_authorize_resource is (Controller Authorization Example), and how to handle nested resources using the load_and_authorize_resource method (Nested Resources), but I was not able to find the per-action equivalent of the load_and_authorize_resource and load a parent resource.
Is there a per-action equivalent to:
class TasksController < ApplicationController
load_and_authorize_resource :project
load_and_authorize_resource :task, :through => :project
end
Thanks!
You probably want to
#project = Project.find(params[:project_id])
authorize! :read, #project
#task = #project.tasks.find(params[:id])
authorize! :action, #task
Related
In my PromoCodesController I have this code:
load_and_authorize_resource :restaurant, find_by: :permalink
load_resource :discount, through: :restaurant
load_resource :promo_code, collection: [:create], through: :discount
It should be good since in #index, it loads the collection #promo_codes and in #create it loads #promo_code.
But it does not load the collection #promo_codes in #create. Where is the problem? In the documentation it says:
:collection argument: Specify which actions are resource collection actions in addition to :index.
Thank you
It's not working because Cancancan's method load_resource (controller_resource_loader.rb) assumes
only one resource variable to be set at a time: either resource_instance or collection_instance.
Your load_resource collection: [:create] can load #promo_codes in #create action via monkey patch to CanCan::ControllerResourceLoader:
# config/initializers/cancan.rb
module CanCan
module ControllerResourceLoader
def load_resource
return if skip?(:load)
# Original condition has been split into two separate conditions
if load_instance?
self.resource_instance ||= load_resource_instance
end
if load_collection?
self.collection_instance ||= load_collection
end
end
end
end
The common way in which this patch works is a create form integrated into index action:
class TicketsController < ActionController::Base
load_and_authorize_resource collection: [:create]
def index
#ticket = Ticket.new
end
def create
if #ticket.valid?
#ticket.create_for_user! params[:message]
redirect_to ticket_path(#ticket)
else
# #tickets are also defined here
render :index
end
end
end
Have a nested resource as such
class Dealer < ActiveRecord::Base
has_many :vehicles
end
and
class Vehicle < ActiveRecord::Base
belongs_to :dealer
end
below are my routes.
resources :dealers do
resources :vehicles, :except => [:index]
end
resources :vehicles, :only => [:index]
looking at the wiki at the github page for cancan I did the following:
class VehiclesController < ApplicationController
load_and_authorize_resource :dealer
load_and_authorize_resource :vehicle, :through => :dealer
def index
#vehicles = Vehicle.all
respond_to do |format|
format.html # index.html.erb
format.json { render json: #vehicles }
end
end
end
but now when the admin tries to go to the index page with the abilities:
def initialize(user)
user ||= User.new # guest user (not logged in)
if user.has_role? :admin
can :manage, :all
end
end
I get
Couldn't find Dealer with id=
what do i need to change for admin to still be able to do all the actions and yet have others be checked before they can do any action.
The problem is not that he is not authorized to this action. The problem is that CanCan tries to fetch an instance of dealer to load all its vehicles and you have not provided a :dealer_id within params[:dealer_id]. Cancan assumes you would be loading only dealer's vehicles in this controller because you used an load_and_authorieze :through. This authorization should be used within Dealers::VehiclesController.
As long as you only loading vehicles just use load_and_authorize_resource :vehicle. And because load_and_authorize will set #vehicles for you within the before filter there is also no need to load the vehicles explicitly with Vehicle.all.
load_and_authorize is just a convenient method and it assumes some defaults. Once you will come to a point where you have some more complex use case. It will be time to throw away this method and just use CanCan's authorize!, can? and a properly configured Vehicle .accessible_by (for listing) methods.
When using load_and_authorize_resource :vehicle, through: :dealer it expects to receive a dealer_id in the request in order to authorize the dealer.
Since you use except: :index in your routes dealer_id will not be automatically included in the request.
If you don't want to authorize the dealer in the index action you can do something like this (taken from Can Can wiki)
class CommentsController < ApplicationController
load_and_authorize_resource :post
load_and_authorize_resource :through => :post
skip_authorize_resource :only => :show
skip_authorize_resource :post, :only => :show
end
I am using Rails Inherited_resource gem in my comments controller, and comments is a nested resource so:
resources :projects do
resources :comments do
end
I also have a belongs_to in the comments controller:
belongs_to :project, :finder => :find_by_project_uuid!, :class_name => "Thfz::Project", :polymorphic => true
How can I set the comment's user association to the current_user(user_id) when its created? As user_id is not suppose to be massive assigned.
I tried following:
def begin_of_association_chain
current_user
end
This does set the user id correctly, but I cannot get nested resource working for Project with this.
Same question come when destroy a comment, I will need to find the comment through current_user, so how to achieve this?
So do I have to write my own create and destroy actions?
Thanks :)
Have you tried the following inside comments_controller?
class CommentsController < InheritedResources::Base
before_filter :authenticate_user! # Assuming you are using Devise for authentication
respond_to :html, :xml, :json
belongs_to :project, :finder => :find_by_project_uuid!, :class_name => "Thfz::Project"
def create
#comment = build_resource
#comment.author = current_user
create!
end
end
I use devise and cancan gems and have simple model association: user has_many subscriptions, subscription belongs_to :user. Have following SubscriptionsController:
class SubscriptionsController < ApplicationController
load_and_authorize_resource :user
load_and_authorize_resource :subscription, through: :user
before_filter :authenticate_user!
def index
#subscriptions = #user.subscriptions.paginate(:page => params[:page]).order(:created_at)
end
#other actions
end
And Cancan Ability.rb:
class Ability
include CanCan::Ability
def initialize(user)
user ||=User.new
can [:read], [Edition, Kind]
if user.admin?
can :manage, :all
elsif user.id
can [:read, :create, :destroy, :pay], Subscription, user_id: user.id
can [:delete_from_cart, :add_to_cart, :cart], User, id: user.id
end
end
end
The problem is that i cannot use subscriptions actions as a user but can as a admin. And have no problems with UsersController. When i delete following lines from SubscriptionsController:
load_and_authorize_resource :user
load_and_authorize_resource :subscription, through: :user
before_filter :authenticate_user!
Have no problems at all. So the issue in these lines or in Ability.rb. Any suggestions?
UPDATE: It's interesting that if i add smth like can? :index, Subscription to html template it displays true. If add smth like can? :index, Subscription.first (subscription of another user) it shows false. Looks like Cancan works normally. But what's the problem?..
UPDATE: If change SubscriptionsControlle like:
class SubscriptionsController < ApplicationController
#load_and_authorize_resource :user
#load_and_authorize_resource :subscription, through: :user
before_filter :authenticate_user!
def show
#user = User.find params[:user_id] #line 1
#subscription = #user.subscriptions.find params[:id] #line 2
#container_items = #subscription.container_items.paginate(:page => params[:page])
authorize! :show, #subscription #line 4
end
#some actions
end
It works perfect and prevent unauthorized user access when need.
Are the lines #1, 2 and 4 not equivalent to commented?..
UPDATE: Have the following in routes.rb:
resources :users, except: [:show] do
member do
get 'cart'
delete 'delete_from_cart' => 'users#delete_from_cart'
post 'add_to_cart' => 'users#add_to_cart'
end
resources :subscriptions do
member do
post 'pay'
end
end
end
UPDATE: Next solution prevent unauthorized access to all of subscriptions actions except index:
class SubscriptionsController < ApplicationController
load_resource :user
load_resource :subscription, through: :user
authorize_resource through: :current_user
before_filter :authenticate_user!
#actions
end
So what's the best way to prevent access to index action?
Found only following solution:
before_filter :authorize_index, only: [:index]
def authorize_index
raise CanCan::AccessDenied unless params[:user_id] == current_user.id.to_s
end
It should be
load_and_authorize_resource :subscription
or just
load_and_authorize_resource
in your case, when you want nested resource, then
load_and_authorize_resource :through => :current_user
see https://github.com/ryanb/cancan/wiki/Nested-Resources
I have a number of resources (Trips, Schedules, etc) with actions that should be limited to just the resource's owner.
How do you implement code with a #require_owner method defined in ApplicationController to achieve this? Ideally, the code will look up the inheritance chain for the owner so the before_filter will work on a :comment that belongs_to :trip that belongs_to :user.
class TripsController < ApplicationController
belongs_to :member
before_filter :require_owner
...
end
I don't fully follow the description (would a comment really be owned by the trip owner?), but expanding slightly on jonnii's answer, here is an example that restricts the trip controller:
class ApplicationController < ActionController::Base
...
protected
# relies on the presence of an instance variable named after the controller
def require_owner
object = instance_variable_get("##{self.controller_name.singularize}")
unless current_user && object.is_owned_by?(current_user)
resond_to do |format|
format.html { render :text => "Not Allowed", :status => :forbidden }
end
end
end
end
class TripsController < ApplicationController
before_filter :login_required # using restful_authentication, for example
# only require these filters for actions that act on single resources
before_filter :get_trip, :only => [:show, :edit, :update, :destroy]
before_filter :require_owner, :only => [:show, :edit, :update, :destroy]
...
protected
def get_trip
#trip = Trip.find(params[:id])
end
end
Assuming the model looks like this:
class Trip < ActiveRecord::Base
belongs_to :owner, :class_name => 'User'
...
def is_owned_by?(agent)
self.owner == agent
# or, if you can safely assume the agent is always a User, you can
# avoid the additional user query:
# self.owner_id == agent.id
end
end
The login_required method (provided by or relying on an auth plugin like restful_authentication or authlogic) makes sure that the user is logged in and provides the user with a current_user method, get_trip sets the trip instance variable which is then checked in require_owner.
This same pattern can be adapted to just about any other resource, provided the model has implemented the is_owned_by? method. If you are trying to check it when the resource is a comment, then you'd be in the CommentsController:
class CommentsController < ApplicationController
before_filter :login_required # using restful_authentication, for example
before_filter :get_comment, :only => [:show, :edit, :update, :destroy]
before_filter :require_owner, :only => [:show, :edit, :update, :destroy]
...
protected
def get_comment
#comment = Comment.find(params[:id])
end
end
with a Comment model that looks like:
class Comment < ActiveRecord::Base
belongs_to :trip
# either
# delegate :is_owned_by?, :to => :trip
# or the long way:
def is_owned_by?(agent)
self.trip.is_owned_by?(agent)
end
end
Make sure to check the logs as you are doing this since association-dependent checks can balloon into a lot of queries if you aren't careful.
There's a few different ways to do this. You should definitely check out the acl9 plugin (https://github.com/be9/acl9/wiki/tutorial:-securing-a-controller).
If you decide you want to do this yourself, I'd suggest doing something like:
class Trip < ...
def owned_by?(user)
self.user == user
end
end
class Comment < ...
delegate :owned_by?, :to => :trip
end
# in your comment controller, for example
before_filter :find_comment
before_filter :require_owner
def require_owner
redirect_unless_owner_of(#commemt)
end
# in your application controller
def redirect_unless_owner_of(model)
redirect_to root_url unless model.owned_by?(current_user)
end
Forgive me if there are any syntax errors =) I hope this helps!
Acl9 is a authorization plugin. I'd give you the link, but I don't have cut and paste on my iPhone. If no one else provides the link by the time I get to a computer, I'll get it for you. Or you can google. Whichever. :)
I have only just started using it, but it has an extremely simple interface. You just have to create a roles table and a roles_user. Let me know how it goes if you decide to use it.
Or just use inherited resources:
InheritedResources also introduces another method called begin_of_association_chain. It’s mostly used when you want to create resources based on the #current_user and you have urls like “account/projects”. In such cases you have to do #current_user.projects.find or #current_user.projects.build in your actions.
You can deal with it just by doing:
class ProjectsController < InheritedResources::Base
protected
def begin_of_association_chain
#current_user
end
end