CanCan, nested resources and using methods - ruby-on-rails

In my new project, I have a resource Bet which other users can only read if they are the owners of the bet or friends of him. The main problem comes when I want to define abilities for the index action. In an index action, the block does not get executed, so I guess it's not an option.
Let's illustrate it. If I wanted only the owner to be able to index the bets, this would be enough:
can :read, Bet, :user => { :id => user.id }
But I need the acceptable ids to be a range, one defined by all the friends of the user. Something like:
if (bet.user == user) || (bet.user.friends.include? user)
can :read, Bet
end
But this is not correct CanCan syntax.
I guess that a lot of people has had problems with CanCan and nested resources, but I still haven't seen any answer to this.

In your Bet model, create a method:
def is_accessible_by? (user)
owner = self.user
owner == user || owner.friends.include?(user)
end
Now in ability.rb, set your CanCan permission:
can :read, Bet { |bet| bet.is_accessible_by?(user) }
EDIT
As you point out, since index actions don't have an instance of the object, the block above won't get executed.
However, it sounds like what you are trying to do - list the bets owned by the user or their friends - should not be handled using CanCan or permissions. I would create a function in my User model:
def bet_listings
friend_bets = friends.inject([]){ |bets, friend| bets<<friend.bets; bets }
self.bets + friend_bets
end
Then in your index action:
#bets = user.bet_listings

Just now I have found a solution, but I don't like it much. It's about creating a custom action and defining abilities for it. For example...
In the controller:
def index
authorize! :index_bets, #user
end
In ability.rb:
can :index_bets, User do |friend|
user == friend || user.friends.include?(friend)
end
It works, but I don't feel great about using it. Isn't out there anything more elegant?

Related

Admin can't create items for self with CanCan?

I'm using CanCanCan for authorization. An admin can manage all, so they don't have per-user-id rules. The result is that they can't create items for self by default. It looks like I need to add a bunch of extra plumbing to make create in my controllers work the same for admins as it does for general users. The reason seems to be Ability#attributes_for doesn't provide the admin user with the user_id attribute.
How are other people getting around this? Are you writing code to specifically handle the admin use case in your view or controller?
Relevant parts of the Ability class
if user.admin?
can manage, :all
else
can manage, Purchase, user_id: user.id
end
Example interaction
2.6.2 :012 > Ability.new(User.find(3)).attributes_for(:create, Purchase)
=> {:user_id=>3}
2.6.2 :013 > Ability.new(User.find(4)).attributes_for(:create, Purchase)
=> {}
User 3 is general_user, User 4 is an admin
In the controller
# relying on load_and_authorize_resource
def create
puts #purchase.user_id # => nil for admin, 3 for general user
# have to add this for admin use case
#purchase.user = current_user
...
end
I wouldn't rely on the Ability object to assign user_ids to your new objects. I think it's better to explicitly write it in the controller. It seems more clear what's happening. So basically just initiate a new object in the create method and don't rely on load_and_auhtorize_resource
def create
#purchase = current_user.purchases.new(purchase_attributes)
end
If the user wasn't authorized, CanCanCan would already have interfered.
ps. I have been a CanCanCan user for years, but recently moved to Pundit. I think the way it was designed is way better and clearer than CanCanCan. Check it out if you have the time!
There's a bug in CanCanCan. Defining the ability can(:manage) needs an id. Creating a record doesn't. So the ability file doesn't allow you to create bc it's looking only for record that it can find with an id. if you define can :create, Purchase above where you define the :manage you should be good :D
if user.admin?
can :manage, :all
else
can :create, Purchase
can :manage, Purchase, user_id: user.id
end

session aware model methods in to_json cause n+1 queries

This is an optimization question for an existing application, I've made the code generic to both make it annonmous and also easier to understand, instead of our proprietary models I'm describing a Forum discussion type situation. I've modified all this code for this example and not tested it, so if there are any typos I apologize, I'll try to fix them if they are pointed out to me.
Lets say I have a rails app with four models: Event, User, Forum, and Post.
the important relationships are as follows:
User has many events.
Forum has many posts.
Post has many events.
The front end is a single page javascript app, so all database data needs to be returned in json format.
Context:
when a User clicks on a post, an event is created with the name
'Show' which marks the post as no longer new.
The user needs to be
logged in to see which posts are new clicking on a forum calls the
following endpoint:
There are multiple users so the events able is a many to many relationship between posts and users.
example.com/forum/15/all_posts
heres the relevant code:
Forum Controller:
#forums_controller.rb
def all_posts
current_user = User.find(session[:user_id])
forum = Forum.includes(:posts).where(id: params[:id]).take
forum.posts.each do |post|
post.current_user = current_user
end
render json: forum.to_json(
include: [
{ posts: {
methods: [:is_new]
}}
]
)
end
Posts model:
#post.rb (posts model)
has_many :events
attr_accessor :current_user
def is_new
if current_user #user may not be logged in
!!self.events.where(user_id: current_user.id, name: 'Show').take
else
false
end
end
the model is where the action is at, so we've tried to keep logic out of the controller, but since the session is not available in the model we end up with this crazy work around of adding current_user as an attr_accessor so that methods can return the appropriate data for the user in question.... I don't like this but I've never come up with a better way to do it. We've repeated this pattern elsewhere and I would love to hear alternatives.
Here's my problem:
The call to is_new is used on the front end to determine what posts to hi-light but it's also triggering an n+1 scenario If there are 10 posts, this endpoint would net me a total 12 queries which is no good if my events table is huge. If I moved all the logic to the controller I could probably do this in 2 queries.
in short I have two questions:
MOST IMPORTANT: How can I fix this n+1 situation?
Is there a better way in general? I don't like needing an each loop before calling to_json I don't find this pattern to be elegant or easy to understand. at the same time I don't want to move all the code into the controller. What is the rails way to do this?
If working with scope is an option, I will try something like:
class Post < ApplicationRecord
scope :is_new, -> { where(user_id: current_user.id, name: 'Show') } if current_user.id?
end
If is a better option to send the current_user in your case, you can also do it:
class Post < ApplicationRecord
scope :is_new, ->(current_user) {...}
end
This is just pseudo-code to give an example:
First Answer
When I posted this I forgot you are rendering json from ForumsController.
Post
scope :for_user, -> (user = nil) do
includes(events: :users).where(users: {id: user.id}) if user
end
def is_new_for_user?(user = nil)
return true if user.nil?
self.events.empty?{ |e| e.name == 'Show' }
end
PostController
def index
#posts = Post.for_user(current_user)
end
posts/index.html.erb
...
<% if post.is_new_for_user?(current_user) %>
...
<% end
...
Second Answer
This is still pseudo-code. I didn't test anything.
Forum
scope :for_user, -> (user = nil) do
if user
includes(posts: [events: :users]).where(users: {id: user.id})
else
includes(:posts)
end
end
ForumsController
def all_posts
current_user = User.find(session[:user_id])
forum = Forum.for_user(current_user).where(id: params[:id]).take
render json: forum.to_json(
include: [
{ posts: {
methods: [:is_new_for_user?(current_user)]
}}
]
)
end

spree customizing the create user sessions action

I added inheritance to my Spree::User model class with STI. I have a :type column which can be (Spree::Guest, Spree::Writer, or Spree::Reader).
In my authentication in the admin side I want to authenticate only writer and reader. What would be the best option to solve this issue?
I tried to override the create action to something like:
def create
authenticate_spree_user!
if spree_user_signed_in? && (spree_current_user.role?(:writer) || spree_current_user.role?(:reader))
respond_to do |format|
format.html {
flash[:success] = Spree.t(:logged_in_succesfully)
redirect_back_or_default(after_sign_in_path_for(spree_current_user))
}
format.js {
user = resource.record
render :json => {:ship_address => user.ship_address, :bill_address => user.bill_address}.to_json
}
end
else
flash.now[:error] = t('devise.failure.invalid')
render :new
end
end
In this case when trying to authenticate with user of type :guest, it redirects to the new action with invalid failure message (ok) but somehow the user get authenticated (nok).
I don't think that is a good way to solve that, controller should be just a controller. I'd rather go that way:
Spree uses cancancan (or cancan in older branches) for authorization and that's how Spree implements that. I don't know why you want that STI solution - I would simply create new custom Spree::Role for that but as I said I don't know why you chose STI way - that should work fine too.
Anyway, you can either just add a decorator for that ability file with additional checks for something like user.is_a? Spree::Guest and so on or register new abilities via register_ability - something like this.
Most important part of third link (or in case it goes off):
# create a file under app/models (or lib/) to define your abilities (in this example I protect only the HostAppCoolPage model):
Spree::Ability.register_ability MyAppAbility
class MyAppAbility
include CanCan::Ability
def initialize(user)
if user.has_role?('admin')
can manage, :host_app_cool_pages
end
end
end
Personally I would go with decorator option (code seems a bit unclear but is cleaner when it comes to determine what can be managed by who - remember about abilities precedence) but it is up to you. If you have any specific questions feel free to ask, I will help if I will be able to.
Edit: so if you want to disable authentication for some users maybe just leverage existing Devise methods? Something like this(in your user model):
def active_for_authentication?
super && self.am_i_not_a_guest? # check here if user is a Guest or not
end
def inactive_message
self.am_i_not_a_guest? ? Spree.t('devise.failure.invalid') : super # just make sure you get proper messages if you are using that module in your app
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.

rails CanCan - Defining a permission for a nested model

I have two models:
Thread (id, title)
ThreadParticipation (id, thread_id, user_id)
I want to define something like:
can :create, ThreadParticipation if the user is a ThreadParticipation
example:
for
thread: (1, 'hello world')
thread_participation: (313, 1, 13) -- where 13 is the user_id
I tried:
can :create, ThreadParticipation, :thread_participations => { :user_id => current_user.id }
But that errors. Any ideas?
Thread in general is just a bad model name, because it will clash with the Thread class defined in Ruby. I implemented this just recently. In my example, I have forums and topics. A person shouldn't be able to create a new topic in a forum they don't have read access to.
I defined a custom permission on the Forum object called create_topic for this:
can :create_topic, Forem::Forum do |forum|
can?(:read, forum) && user.can_create_forem_topics?(forum)
end
Where can_create_forem_topics? is just defined on the User model like this:
def can_create_forem_topics?(forum)
# code goes here
end
Then I just use this custom :create_topic ability in the new and create actions in my TopicsController, where #forum is defined by a before_filter:
authorize! :create_topic, #forum
And if I need to use it in views:
can? :create_topic, #forum
by defining a custom permission on the parent object,
I'm currently trying to achieve something similiar, but I don't have the whole sight into this. Try this:
can :create, ThreadParticipation => Thread, do |participation, thread|
# your definition here
end
This should also work without a block.
EDIT: drop the part above, that doesn't work yet in CanCan. I implemented my own solution, but it requires you to authorize controller actions manually, which is not as beautify, but more secure in my opinion.
I implemented it this way for my project:
https://gist.github.com/822208
Normally you would user
user
not
current_user
within ability.rb. Is this your error? As well your ability is specified incorrectly. You want
can :create, ThreadParticipation, :user_id => user.id
Note that :user_id is a property of ThreadParticipation model

Resources