I have a non-restful controller that I am trying to use the cancan authorize! method to apply permissions to.
I have a delete_multiple action that starts like so
def delete_multiple
#invoices = apparent_user.invoices.find(params[:invoice_ids])
I want to check that the user has permission to delete all of these invoices before proceeding. If I use
authorize! :delete_multiple, #invoices
permission is refused. My ability.rb includes the following
if user.admin?
can :manage, :all
elsif user.approved_user?
can [:read, :update, :destroy, :delete_multiple], Invoice, :user_id => user.id
end
Is it a matter of looping through my array and calling authorize individually or is there a smarter way of doing things? I'm starting to feel like doing authorizations would be easier manually than by using cancan for a complicated non-restful controller (although I have plenty of other restful controllers in my app where it works great).
A little late in here but you can write this in your ability class
can :delete_multiple, Array do |arr|
arr.inject(true){|r, el| r && can?(:delete, el)}
end
EDIT
This can be written also as:
can :delete_multiple, Array do |arr|
arr.all? { |el| can?(:delete, el) }
end
It seems that authorize! only works on a single instance, not an array. Here's how I got around that with Rails 3.2.3 and CanCan 1.6.7.
The basic idea is to count the total records that the user is trying to delete, count the records that are accessible_by (current_ability, :destroy), then compare the counts.
If you just wanted an array of records that the user is authorized to destroy, you could use the array returned by accessible_by (current_ability, :destroy). However I'm using destroy_all, which works directly on the model, so I wound up with this count-and-compare solution.
It's worthwhile to check the development log to see how the two SELECT COUNT statements look: the second one should add WHERE phrases for the authorization restrictions imposed by CanCan.
My example deals with deleting multiple messages.
ability.rb
if user.role_atleast? :standard_user
# Delete messages that user owns
can [:destroy, :multidestroy], Message, :owner_id => user.id
end
messages_controller.rb
# Suppress load_and_authorize_resource for actions that need special handling:
load_and_authorize_resource :except => :multidestroy
# Bypass CanCan's ApplicationController#check_authorization requirement:
skip_authorization_check :only => :multidestroy
...
def multidestroy
# Destroy multiple records (selected via check boxes) with one action.
#messages = Message.scoped_by_id(params[:message_ids]) # if check box checked
to_destroy_count = #messages.size
#messages = #messages.accessible_by(current_ability, :destroy) # can? destroy
authorized_count = #messages.size
if to_destroy_count != authorized_count
raise CanCan::AccessDenied.new # rescue should redirect and display message
else # user is authorized to destroy all selected records
if to_destroy_count > 0
Message.destroy_all :id => params[:message_ids]
flash[:success] = "Permanently deleted messages"
end
redirect_to :back
end
end
Related
I have everything working correctly, but now I want to limit some user Abilities to perform some attachment actions.
Specifically, the ability to limit the viewing of all uploaded attachments, to those actually uploaded by the User.
Here is the applicable snippet from ability.rb I tried ...
if user.id
can :access, :ckeditor
can [:read, :create, :destroy], Ckeditor::Picture, assetable_id: user.id
can [:read, :create, :destroy], Ckeditor::AttachmentFile, assetable_id: user.id
end
The situation arises when I am using the CKeditor UI, click the Image button, and then click the Browse Server button to see the previously uploaded images -- right now the image browser shows the uploads of all users. I would like the viewed images to be limited to those of the current_user only.
Since the Ckeditor table saves the assetable_id of the attachment (i.e. the user.id), and the logic above does not work on its own, I'm guessing some custom Controller logic is also needed here.
Thanks.
I was able to solve this issue with custom Ckeditor controllers & some guidance from here:
https://github.com/galetahub/ckeditor/issues/246
First I needed to make copies of the Ckeditor controllers pictures_controller.rb & attachment_files_controller.rb and place them here:
/app/controllers/ckeditor/
Then a few updates to their suggestions to update index were necessary, particularly picture_model.find_all needed to be picture_adapter.find_all in pictures_controller.rb (and similarly attachment_file_adapter.find_all in attachment_files_controller.rb)
The key to it all is setting the proper scope with: ckeditor_pictures_scope(assetable_id: ckeditor_current_user) & ckeditor_attachment_files_scope(assetable_id: ckeditor_current_user)
Once these revisions are in place, the file browsers for pictures & attachments show only the appropriate files for that user.
Here are the revised files ... the changes are on line 4 of both.
/app/controllers/ckeditor/pictures_controller.rb
class Ckeditor::PicturesController < Ckeditor::ApplicationController
def index
#pictures = Ckeditor.picture_adapter.find_all(ckeditor_pictures_scope(assetable_id: ckeditor_current_user))
#pictures = Ckeditor::Paginatable.new(#pictures).page(params[:page])
respond_with(#pictures, :layout => #pictures.first_page?)
end
def create
#picture = Ckeditor.picture_model.new
respond_with_asset(#picture)
end
def destroy
#picture.destroy
respond_with(#picture, :location => pictures_path)
end
protected
def find_asset
#picture = Ckeditor.picture_adapter.get!(params[:id])
end
def authorize_resource
model = (#picture || Ckeditor.picture_model)
#authorization_adapter.try(:authorize, params[:action], model)
end
end
/app/controllers/ckeditor/attachment_files_controller.rb
class Ckeditor::AttachmentFilesController < Ckeditor::ApplicationController
def index
#attachments = Ckeditor.attachment_file_adapter.find_all(ckeditor_attachment_files_scope(assetable_id: ckeditor_current_user))
#attachments = Ckeditor::Paginatable.new(#attachments).page(params[:page])
respond_with(#attachments, :layout => #attachments.first_page?)
end
def create
#attachment = Ckeditor.attachment_file_model.new
respond_with_asset(#attachment)
end
def destroy
#attachment.destroy
respond_with(#attachment, :location => attachment_files_path)
end
protected
def find_asset
#attachment = Ckeditor.attachment_file_adapter.get!(params[:id])
end
def authorize_resource
model = (#attachment || Ckeditor.attachment_file_model)
#authorization_adapter.try(:authorize, params[:action], model)
end
end
I wanted to see what the community here would suggest for best approach. I've got my app right now where Users have plan_id, so plan_id = "1" would be a trial or free and plan_id = "2" would be subscription plan. Now my controllers check for two things, 1.) the plan_id if its 1 or 2... 2.) if on the free plan, check the number of posts they have saved in the DB and 3.) if on free plan checks the # of categories they have.
I want to limit my users to the # of posts and categories, if they hit that limit they have to subscribe to unlimited plan = plan 2.
Now I was also looking at Cancan or Easy_Roles but in my case I'm not sure that would work.
Should I stay with my solution that works or change it and implement Cancan or Easy_roles? I'm looking for suggestions.
Thank you,
You should be able to use CanCan for this if you'd like. The way to set it up is to define the abilities in a following way (untested code, but you get the idea):
class Ability
include CanCan::Ability
def initialize(user)
can :manage, :all
cannot :create, Post do |p|
user.plan_id == 1 and user.posts.count > X
end
cannot :create, Category do |c|
user.plan_id == 1 and user.categories.count > Y
end
end
end
The disadvantage of this is that when the user is not authorized a generic exception is thrown and you don't know why it was thrown. So when you handle it you have to either check again:
rescue_from CanCan::AccessDenied do |exception|
if user.plan_id == 1 and user.posts.count > X
render 'too_many_posts'
elsif user.plan_id == 1 and user.categories.count > Y
render 'too_many_categories'
else
...
end
end
or pass it in the message when checking in the controller:
authorize! :create, Post, :message => "Unable to create so many posts"
I'm having problems restricting the data shown to a specific user group using cancan..
My Users have many Products. And Products have many Vouchers.
In my routes.rb I have this:
resources :products do
resources :vouchers
end
In ability.rb:
can [:create, :update, :read ], Voucher, :product => { :user_id => user.id }
And in my Voucher controller:
def index
...
if params[:product_id]
#voucher = Voucher.find_all_by_product_id(params[:product_id])
end
...
end
Finally, in my view, I'm trying to display a list of vouchers in a Product group associated with current user.
For example:
http://localhost:3000/products/eef4e33116a7db/voucher
This lists the vouchers in the product group however, ALL users can see every voucher / product..
I'll assume my abilities are wrong. Help please :)
Have a look at the can can wiki for fetching records: https://github.com/ryanb/cancan/wiki/Fetching-Records
If you're calling load_and_authorize_resource, you'll either be able to do something similar to one of these two things:
def index
...
if params[:product_id]
#vouchers = #vouchers.where(product_id: params[:product_id])
end
...
end
load_and_authorize_resource should automatically assign the #vouchers instance variable based on the accessible_by parameters for that controller action. If this isn't working, just define it more explicitly:
if params[:product_id]
#vouchers = Voucher.includes(:product).where(product_id: params[:product_id]).where(product: { user_id: current_user.id })
end
For anyone else having this issue, I needed to do the following in my vouchers controller:
#product = Product.accessible_by(current_ability).find(params[:product_id])
#voucher = #product.vouchers
However, although this did actually block other users from viewing the results, loading the product first with accessible_by led to an exception which required a separate rescue block.
I have a model, Report, that is polymorphic.
So many itens in my site may have many of it.
And i would like to have a generic controller for posting it.
Its a very simple model, has only a text message and the association.
in my routes, im doing something like
map.resources :users, :has_many => [ :reports ]
map.resources :posts, :has_many => [ :reports ]
but in my reports_controller, i would like to get the relation from with its coming from.
like:
before_filter :get_reportable
def get_reportable
reportable = *reportable_class*.find params[:reportable_id]
end
is this possible?
how could i get the reportable_class and the reportable_id?
I can get the params[:user_id] when it comes from users controller, or params[:post_id] when it comes from posts. I could do a case with all the relations, but it doesnt seem a clean solution at all...
having the polymorphic association would be the best, are there any how?
If you have a single controller that processes requests through two differing paths, then you need to make it aware of the contexts in which it will be called. You often see a lot of code that looks something like this:
before_filter :load_reportable
def load_reportable
if (params[:user_id])
#user = User.find(params[:user_id])
#reportable = #user
elsif (params[:post_id])
#post = Post.find(params[:post_id])
#reportable = #post
end
rescue ActiveRecord::RecordNotFound
render(:partial => 'not_found', :status => :not_found)
return false
end
Since you're using a polymorphic association, you may be able to do something like this instead:
before_filter :load_reportable
def load_reportable
unless (#reportable = #report.reportable)
# No parent record found
render(:partial => 'not_found', :status => :not_found)
return false
end
# Verify that the reportable relationship is expressed properly
# in the path.
if (params[:user_id])
unless (#reportable.to_param == params[:user_id])
render(:partial => 'user_not_found', :status => :not_found)
return false
end
elsif (params[:post_id])
unless (#reportable.to_param == params[:post_id])
render(:partial => 'post_not_found', :status => :not_found)
return false
end
end
end
The trouble with this approach, where you have one controller that serves two entirely different routes, is that generating error messages, such as "user not found" versus "post not found". This can be tricky to get right if you're not inheriting from a Users::BaseController, for instance.
In many cases it's easier to create two independent "reports" controllers, such as users/reports and posts/reports, where any common functionality is imported from a module. These controllers usually inherit from a base controller which performs the loading and error handling. The base controller can also establish layout, page title, etc., without having to re-implement this functionality for each sub-resources controller.
The alternative is to de-couple reports and have it run as its own controller where the relationship to the "reportable" record is mostly irrelevant.
Or try that:
before_filter :get_reportable
def get_reportable
params.each do |name, value|
if name =~ /(.+)_id$/
#reportable = $1.classify.constantize.find(value)
end
end
end
It is going through all the params and tries to find one ending with _id, then grabs that before part and finds relevant record.
I have a fairly simple model; Users have_many products. I would like to be able to view a list of all products as well as a list of the products associated with a given user. My routes are set up like this:
/products
/products/:id
/users
/users/:id
/users/:id/products
The catch here is that I'd like to display the product list differently in the product#index view and the user/products#index view.
Is there a 'correct' way to do this? My current solution is to define products as a nested resource inside users, and then to check for params[:user_id] - if its found I render a template called 'index_from_user', otherwise I just render the typical 'index' template.
This is a situation I'm running into a lot - if there's a preferred way to do it I'd love to know...
You can declare two "products" routes - one under users, and one independent of users eg:
map.resources :products
map.resources :users, :has_many => :products
They will both look for "ProductsController#index" but the second will have the "user_id" pre-populated from the route (note: "user_id" not just "id")
So you can test for that in the index method, and display different items depending on whether it is present.
You will need to add a before_filter to the ProductController to actually instantiate the user model before you can use it eg:
before_filter :get_user # put any exceptions here
def index
#products = #user.present? ? #user.products : Product.all
end
# all the other actions here...
# somewhere near the bottom...
private
def get_user
#user = User.find(params[:user_id])
end
If you really want to display completely different views, you can just do it explicitly in the index action eg:
def index
#products = #user.present? ? #user.products : Product.all
if #user.present?
return render(:action => :user_view) # or whatever...
end
# will render the default template...
end