How do I use cancan to authorize an array of resources?
Asked Answered
W

2

7

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).

Winnifredwinning answered 27/3, 2011 at 1:42 Comment(0)
C
13

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
Cisco answered 15/9, 2011 at 10:10 Comment(7)
I can't even remember how I dealt with this in the end but that code twisted my brain so badly I have to vote it up!Winnifredwinning
So I've got a similar :read_multiple ability defined. At the top of my controller I've got load_and_authorize_resource :through => :tickets, :except => :index. And in my index method I've got @claims = Claim.where(:claimant_provider_id => current_user.provider.id); authorize!(:read_multiple, @claims) unless @claims.empty?. However, it always fails to authorize even when by all means it should. I've tried adding either debugger or a call to raise inside the read_multiple block, but neither are ever triggered, so I'm not sure that ability is even being used. Any ideas?Eta
Never mind, solved my own problem. The ability defined above checks against an Array class, but @claims is technically an ActiveRecord::Relation class. Changing it to @claims.all makes it behave as expected.Eta
Thanks for the update! Upvoting a 22-month-old answer and receiving an instant edit gives me a SO-ner. Would +1 again if i could.Tanker
@Winnifredwinning I'd argue that if this code "twisted" your brain, then it doesn't deserve an upvote. Complexity shouldn't be rewarded. Keep it simple and straight forward. I'd hate to look at this code a few months from now and try and figure out what I was trying to accomplish.Yorgen
Calling @claims.to_a is more explicit.Shrader
Also a good place to put the ability definition would be in the initializer.Shrader
C
2

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 
Concertmaster answered 17/8, 2012 at 2:17 Comment(1)
Thanks for this Mark! I went a similar route, but created it as an extension to my abilities file. You can find the implementation with example here: gist.github.com/chriscz/ac35172e0870248af16b23475a7ddb72Cistaceous

© 2022 - 2024 — McMap. All rights reserved.