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
Related
I need to handle a particular case of generating email views with URLs constructed from non-persisted data.
Example : assume my user can create posts, and that triggers a post creation notification email, I'd like to send the user an example of fake post creation. For this, I am using a FactoryGirl.build(:post) and passing this to my PostMailer.notify_of_creation(#post)
In everyday Rails life, we use the route url_helpers by passing as argument the model itself, and the route generator will automatically convert the model into its ID to be used for the route URL generation (in article_path(#article), the routes helper converts #article into #article.id for constructing the /articles/:id URL.
I believe it is the same in ActiveRecord, but anyways in Mongoid, this conversion fails if the model is not persisted (and this is somewhat nice as it prevents the generation of URLs that may not correspond to actual data)
So in my specific case, URL generation crashes as the model is not persisted:
<%= post_url(#post_not_persisted) %>
crashes with
ActionView::Template::Error: No route matches {:action=>"show", :controller=>"posts", :post_id=>#<Post _id: 59b3ea2aaba9cf202d4eecb6 ...
Is there a way I can bypass this limitation only in a very specific scope ? Otherwise I could replace all my resource_path(#model) by resource_path(#model.id.to_s) or better #model.class.name but this doesn't feel like the right situation...
EDIT :
The main problem is
Foo.new.to_param # => nil
# whereas
Foo.new.id.to_s # => "59b528e8aba9cf74ce5d06c0"
I need to force to_param to return the ID (or something else) even if the model is not persisted. Right now I'm looking at refinements to see if I can use a scoped monkeypatch but if you have better ideas please be my guest :-)
module ForceToParamToUseIdRefinement
refine Foo do
def to_param
self.class.name + 'ID'
end
end
end
However I seem to have a small scope problem when using my refinement, as this doesn't bubble up as expected to url_helpers. It works fine when using te refinement in the console though (Foo.new.to_param # => 59b528e8aba9cf74ce5d06c0)
I found a way using dynamic method override. I don't really like it but it gets the job done. I am basically monkeypatching the instances I use during my tests.
To make it easier, I have created a class method example_model_accessor that basically behaves like attr_accessor excepts that the setter patches the #to_param method of the object
def example_model_accessor(model_name)
attr_reader model_name
define_method(:"#{model_name}=") do |instance|
def instance.to_param
self.class.name + 'ID'
end
instance_variable_set(:"##{model_name}", instance)
end
end
Then in my code I can just use
class Testing
example_model_accessor :message
def generate_view_with_unpersisted_data
self.message = FactoryGirl.build(:message)
MessageMailer.created(message).deliver_now
end
end
# views/message_mailer/created.html.erb
...
<%= message_path(#message) %> <!-- Will work now and output "/messages/MessageID" ! -->
I'm using an enum in my model defined as such:
enum role: [:member, :content_creator, :moderator, :admin]
I wanted an easy way to get the next role from a user's current role, so I came up with this:
def self.next_role(user)
begin
User.roles.drop(User.roles[user.role] + 1).to_enum.next
rescue StopIteration
nil
end
end
In the view, I'd likely add onto this the following method chain: [...].first.humanize.titleize.
I'm only somewhat concerned about my solution here, but mainly wanted to know if there was a better (read: more built-in) way to get what I'm after? I know there are only four enums there and I admit I started my implementation with if ... elsif ... etc.. In other words, I find myself more proficient in Rails than I do with Ruby itself. Can someone elaborate how one "should" do this?
User.roles is just an ActiveSupport::HashWithIndifferentAccess that looks like:
{ 'member' => 0,
'content_creator' => 1,
'moderator' => 2,
'admin' => 3 }
Using that, this solution is pretty close to yours but without the exception handling. I would also be doing this as an instance method on User, not a class method.
Returns the subsequent role as a String, nil if the current role is :admin
def next_role
User.roles.key(User.roles[role] + 1)
end
You can then call (ruby 2.3 required for the &. safe navigation operator)
user.next_role&.humanize
How's this grab you? Okay so it's not really more "built in." But I thought it was different enough from your solution (which was pretty clever imo) to throw into the mix, at least to offer some different elements to potentially incorporate.
def self.next_role(user)
next_role = user.role.to_i + 1
next_role == self.roles.length ? nil : self.roles.invert[next_role]
end
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.
I'm trying to get cancan incorporated into my first ever Ruby on Rails app.
I'm having a problem getting started... its surely something basic.
My application has a list of projects, and a user may or may not have permission to see any number of them.
I added this to my ProjectsController:
class ProjectsController < ApplicationController
load_and_authorize_resource
My initialize method looks like this:
def initialize(user)
user ||= User.new # guest user
puts "******** Evaluating cancan permissions for: " + user.inspect
can :read, Project do |project|
puts "******** Evaluating project permissions for: " + project.inspect
# project.try(project_users).any?{|project_user| project_user.user == user}
1 == 1 #POC test!
end
end
When I have this, the project index page appears, but no projects are listed.
2 questions I have here:
Shouldn't all of the projects appear since true is returned for all
projects?
The second puts statement is not written to the rails
server console, but the first one is. Why is that???
If I change the initialize method to:
def initialize(user)
user ||= User.new # guest user
puts "******** Evaluating cancan permissions for: " + user.inspect
can :read, Project
end
... I see all of the projects as I would expect
If I remove the can :read, Project line, I get a security exception trying to hit the projects index page.... also what I'd expect.
The block being passed to the :read ability is only evaluated when an instance of the project is available (#project). Because you are talking about the index action, only the collection is available (#projects). This explains why your second puts statement is never appearing. In order to limit your index actions, you need to either pass a hash of conditions into the can method, or use a scope (in addition to the block). All of this information is clearly outlined in the CanCan wiki on Github.
So the puts problem is explainable. What does not make sense is how no projects are showing. When evaluating the index action, CanCan will actually default to ignoring the block entirely. This means that your ability is essentialy can :read, Project anyway (even in the first example) for the index action.
I would be interested to have you try to add a simple scope, just to see if it will work. Try:
can :read, Project, Project.scoped do |project|
true
end
And then see what happens for the index action.
edit:
Given that you can see the projects in the index now, it seems like you need to pass a scope into the ability as well as a block. Please read this Github issue where Ryan explains why the block is not evaluated on the index action.
Blocks are only intended to be used for defining abilities based on an
object's attributes. [...] That is the only case when a block should
be used because the block is only executed when an object is
available. All other conditions should be defined outside the block.
Keep in mind that if your ability is not too complex for a hash of conditions, you should use that instead. The hash of conditions is explained on this CanCan wiki page on Github. If you do need a scope, you will need to pass in the scope and the block. Lets say you have the ability shown above.
On the index action, CanCan will disregard the block because a Project object (#project) is not available. It will instead return projects that are within the scope given, in this case Project.scoped (which will just be all projects).
On the show action, #project is available, so CanCan will evaluate the block and allow the action if the block evaluates to true.
So the reason you need to pass both is so that CanCan can handle both the index and show actions. In most cases, your block will define the same thing as the scope does, only the block will be written in Ruby while your scope will be written Rails' ActiveRecord syntax. You can fine more information about here: Defining Abilities with Blocks.
I am querying my booking model to get details, including a list of has_many appointments.
To do this I'm using a scope:
scope :current_cart,
Booking.includes(:appointments).where(:parent_id => 1).where(:complete =>nil).order("created_at DESC").limit(1)
Then in the view:
<% #booking.appointments.each do |appointment| %>
# info output
<% end %>
To get this working, in the controller, I have to do this:
#booking = Booking.current_cart[0]
It's the [0] bit i'm worried about. I guess I'm using a method that wants to return a collection, and that means i have to state that i want the first (only) record. How can i state a similar scope that is more appropriate to fetch a member?
Try tacking ".first" onto the end of the scope. Scopes are just regular AREL queries, and so you can use any standard methods as you normally would.
Adding .first or [0] to the scope gives error:
undefined method `default_scoped?' for
googling that gave this:
undefined method `default_scoped?' while accessing scope
So apparently adding .first or [0] stops it being chainable, so it gives an error. using that answer, i did:
scope :open_carts,
Booking.includes(:appointments).where(:parent_id => 1)
.where(:complete =>nil).order("created_at DESC")
def self.current_cart
open_carts.first
end
Little bit messy, but i'd prefer mess in my model, and it isn't nonsensical to look at.