Issues when using CanCan with enums with both can? and accessible_by() - ruby-on-rails

I'm trying to use CanCan to restrict comments visibility on my site.
In my Comment model is defined an enum:
enum access_right: {nobody: 0, somebody: 1, everyone: 2}
Here is an extract of my current ability.rb file:
class Ability
include CanCan::Ability
def initialize(user, session={})
user ||= User.new # guest user (not logged in)
# Comments
can [:create, :read, :update, :destroy], Comment, person_id: person.id
can [:read], Comment, access_right: [:everyone, Comment.access_rights[:everyone]]
...
end
end
At first I was just using:
can [:read], Comment, access_right: 2
But even though this works when fetching records:
Comment.accessible_by(...)
it doesn't work when checking permission against an object like this:
can? :read, #a_comment
Because then #a_comment.access_right == "everyone" (and not 2)
So I looked online and found this : CanCanCommunity issue 102.
The proposed solution didn't work for me, as using "everyone" directly like this:
can [:read], Comment, access_right: ["everyone", Comment.access_rights[:everyone]]
would give incorrect results. The sql query behind it when fetching records would look like this:
SELECT `comments`.* FROM `comments` WHERE `comments`.`access_right` IN (0,2))
"everyone" seems to be casted to an integer (0) in this case.
I then found a workaround, my current ability file, thanks to symbols (:everyone).
But then things won't work anymore when using can? :read, #a_comment (the sql is correct when using accessible_by)
Does anyone know how to correct this problem?
How to define abilities based on enums which can be both verified while fetching records and with can?
Thank you!
EDIT: It may be related to this CanCanCommunity issue 65 but I can't make it work.

It can be achieved with CanCan block syntax:
# Please note this is an Array of Strings, not Symbols
allowed_access_rights = %w[everyone]
can :read, Comment, ["access_right IN (?)", Comment.access_rights.values_at(allowed_access_rights)] do |comment|
comment.access_right.in? allowed_access_rights
end
Although it's tempting to define scope on model and pass it here instead of using raw SQL condition, as in example:
can :read, Comment, Comment.with_access_right(allowed_access_rights) do |comment| … end
it is actually a poor idea because it is far less flexible, see CanCan docs.

Related

Using scopes in cancan, ability.rb

I'm using cancancan for authorisation.
I want to allow anyone read access to users within a scope.
I have this in user.rb -
class User < ActiveRecord::Base
scope :published, -> { describes scope, works happily
}
end
The cancancan docs describe using scopes like this -
can :read, Photo, Photo.unowned do |photo|
photo.groups.empty?
end
... a block acting on the scope. I just want the scope.
This seems to allow anyone to be read -
can :read, User, User.published do |user|
true
end
I can't reason what block I'm aiming for.
I was making it one step more complicated than it needed to be. All that is required is -
can :read, User.published
Which on reflection makes perfect sense, but I could not get my head around.

Cancan, issue with checking class abilities

I have this code:
can :create, Project { |instance| some_statement }
Later I want to add such condition: if user can create some Project, then he can read ProjectTemplate. But I can't do this like this:
if can? :create, Project
can :read, ProjectTemplate
end
Because if-condition always true. Also, I can't just put can? :create, Project.new, because in my can :create, Project {} statement is not just .new object.
Any suggestions? Am I doing something wrong?
To look at it a slightly different way, can you not have a role whereby the user is able to create projects (e.g. project_creator) and then just use the following
if user.role? :project_creator
can :create, Project { |instance| some_statement }
can :read, ProjectTemplate
end
I realize this isn't exactly what you want, but I am assuming it's not so much of a security risk for a wise group of people to read templates?
I'm also a bit confused that you have a condition on creating projects, the instance statement means that some logic is being applied on a specific instance. However, if a project has not been created there is no instance?
The way I have used that method is similar to the following
can [:read], User do |thisuser|
thisuser == user
end

How can I know if a user has access to all or just some resources with Cancan?

I have a Rails 3 application that uses Cancan for authorization. I have the following logic in my application: An admin has access to manage all other users, and has can :manage, User set in the ability file. A company owner has access to manage all users under his company, and has can :manage, User, company_id: user.company_id set in the ability file.
In my UserController#index, i need to call one method if the user has access to manage all the other users, and another method if the user only can access users from his company. Is there any way to do this with CanCan?
As discussed in the question comments, this sounds like a case of roles vs abilities.
If there already is a role system in place, and the logic for choosing between methods maps directly to these roles, then going through CanCan abilities is unnecessarily complex. CanCan is good at checking for abilities on specific model objects, classes and collections but not at going back to the original logic behind why those abilities were awarded in the first place.
In this specific case, there would need to be a way to refer to the case "can manage all Users in Company X but NOT all Users". It might be possible to accomplish with some if-else structure, but I don't think it is what you actually want. Especially if your ability logic changes over time it may not make sense anymore. One example is the corner case where all users belong to the same company, would it be desirable that the "all Users" method is called even for non-admin company owners?
My suggestion therefore is to check the roles directly, much like you already do in your Abilities class. But I feel your pain. ;)
i'm using cancan with device, in my projet i used probably the same you want
if user_signed_in?
if current_user.has_role? :admin
#users = User.all
end
else
#users = User.where(current.user.company_id == company_id)
end
In controller action you should do something like:
#load #user variable
begin
authorize! :manage, #user
#code when access is granted
rescue CanCan::AccessDenied
#code when access is denied
end
I know this an old question but I also ran into this situation. I was also looking to do all role checking in ability.rb file in one place.
What I ended up doing was far from ideal, but just noting it down if anyone wants to go that road. I defined a new ability like this in the ability.rb:
can :manage, :all_users if user.admin?
Please note that :all_users is just a random name that I chose and not some magic method in cancan.
After defining this ability, I was able to do like this in the controller:
if can? :manage, :all_users
call_method_which_can_access_all_users
else
call_method_which_can_access_only_some_users
end
But, it would have been great if Cancan gave us something like can? :manage, User, :all depending on whether there is a hash for filtering users after the User parameter.

How to pass single method with multiple previleges to 'can?' method of cancan?

I am using devise and cancan gem to manage authentication and permissions in my rails application. The application is about library management. I have defined methods inside ability.rb like:
def lend
can :manage, Book
can :manage, Transactions
end
Then in ability.rb, I have assigned permissions as
if user.role? == "librarian"
can lend
end
In the views, I can use can?(:manage, Book) or can?(manage, Transaction) for users with "librarian" role.
However, I want to be able to use current_user.can?(:lend). I followed this github page to make can? method available inside user model. This allows me to use
current_user.can?(:manage, Book)
But, current_user.can?(:lend) results into wrong number of arguments(1 for 2+). I have a feeling that I am missing a bit of intellect here. I tried google and the related post here but to no avail. Please help.
can? has two required arguments:
def can?(action, subject, *extra_args)
Hence: wrong number of arguments(1 for 2+)
See cancan/lib/cancan/ability.rb:56
Also, in your ability.rb (below) there is neither an action nor a subject named :lend. The method name is irrelevant to cancan.
def lend
can :manage, Book
can :manage, Transactions
end
if user.role? == "librarian"
can lend
end

Using blocks to define abilities in CanCan raises an exception

I am using Ryan Bate's CanCan gem to define abilities and some basic functionality is failing. I have a Product model and Products controller where my index action looks like this:
def index
#req_host_w_port = request.host_with_port
#products = Product.accessible_by(current_ability, :index)
end
I get an error when I try to retrieve the products with the accessible_by method, I get this error:
Cannot determine SQL conditions or joins from block for :index Product(id: integer, secret: string, context_date: datetime, expiration_date: datetime, blurb: text, created_at: datetime, updated_at: datetime, owner_id: integer, product_type_id: integer, approved_at: datetime)
My ability class looks like this:
can :index, Product do |product|
product && !!product.approved_at
end
This seems like a very simple example so I am surprised it is failing and wondering if I am overlooking something simple (i.e. staring at my code for too long).
I did probe a bit more and ran a simple test. If you look at the code below, one example works fine and one fails where they should actually do the same exact thing.
# This works
can :index, Product, :approved_at => nil
# This fails
can :index, Product do |product|
product && product.approved_at.nil?
end
So the problem seems to be in the way CanCan is processing these blocks. I dove deeper into the library and found where the error was raised - in CanCan's ability class definition:
def relevant_can_definitions_for_query(action, subject)
relevant_can_definitions(action, subject).each do |can_definition|
if can_definition.only_block?
raise Error, "Cannot determine SQL conditions or joins from block for #{action.inspect} #{subject.inspect}"
end
end
end
So I checked out what this only_block? method is. The method returns true if a can definition has a block but no conditions which makes no sense to me because the whole point of the block is to define the conditions within the block when they are too complicated for the following syntax:
can :manage, [Account], :account_manager_id => user.id
Any insight on this issue would be great! I also filed an issue on the CanCan github page but I'm getting to the point where I may need to ditch the library. However, I understand that many people are using CanCan successfully and this is such basic functionality I think I must be doing something wrong. Especially since the git repo was updated 3 days ago and Ryan Bates mentions Rails 3 support in the README.
Thanks!
Ok so I went through the wiki and saw that accessible_by will not work when defining blocks. Seems a bit odd to have this restriction and not mention it in the README, but at least I know it's a limitation of the library and not a bug in mine or Ryan Bates' code. For those interested, the proper way to define my ability above is as follows:
# will fail
can :index, Product do |product|
product && product.approved_at.nil?
end
# will pass
can :index, Product
cannot :index, Product, :approved_at => nil
I've run into this problem as well, when sometimes you can only express an ability/permission via a block (Ruby code) and can't express it as an SQL condition or named scope.
As a workaround, I just do my finding as I would normally do it without CanCan, and then I filter that set down using select! to only contain the subset of records that the user actually has permission to view:
# Don't use accessible_by
#blog_posts = BlogPost.where(...)
#blog_posts.select! {|blog_post| can?(:read, blog_post)}
Or more generically:
#objects = Object.all
#objects.select! {|_| can?(:read, _)}
I filed a ticket to get this added to the documentation:
https://github.com/ryanb/cancan/issues/issue/276

Resources