cancancan row based filter - ruby-on-rails

I have declared a role ability in the cancancan gem (rails 5, postgres, devise):
can [:update, :show, :destroy, :index], SalesDatum do |datum|
datum.try(:user_id) == user.id #the ids just access the id's from the user object that is passed into can can and from the table
end
SalesDatum has a user_id field.
This works on the show actions, because I can only show the SalesDatum that has the logged in user_id For example:
http://localhost:8080/projects/19/sales_data/961
correctly gets authenticated because the logged in user_id matches the user_id on the sales_data
http://localhost:8080/projects/19/sales_data/800
correctly does not get authenticated because the logged in user_id does not match the user_id on the sales_data
However when I go to the get index action:
http://localhost:8080/projects/19/sales_data
it shows all of the sales data from the #sales_data variable. So, it would show the data.id 800 and 961.
sales data controller:
load_and_authorize_resource
def index
#sales_data = SalesDatum.where(project_id:params[:project_id])
end
How do I get the index action to only show the data with the relevant user_id? Isn't cancan supposed to filter that according to the user_id?

You need to define the rule with the hash syntax.
can [:update, :show, :destroy, :index], SalesDatum, user_id: user.id
you may also want to define the edit action and therefore:
can [:edit, :update, :show, :destroy, :index], SalesDatum, user_id: user.id
which can be simplified into:
can :manage, SalesDatum, user_id: user.id
if SalesDatum has a belongs_to :user defined you can also write:
can :manage, SalesDatum, user: user

Related

Allow member routes to have authorization equivalent to read action cancancan

module Abilities
class ProjectsAbility < Ability
def abilities
can :manage, Project, user_id: user.id
if user.is?(:super_admin)
can :read, Project
end
if user.is?(:district_admin)
can [:read, :manage_details, :download], Project, user: { district: { id: user_district_ids } }
end
if user.is?(:school_admin)
can [:read, :manage_details, :download], Project, user: { school_id: user_school_ids || user.district.school_ids }
end
if user.is?(:advisor)
can [:read, :manage_details, :download], Project, user: { advisors: { id: user.id } }
end
# alias_action :manage_details, :download, to: :read
end
end
end
As district_admin, school_admin and advisor has same type of abilities. It can be agreed upon that all the users that have read access, have access to manage_details and download functionality.
So I was thinking that maybe I could achieve something like
alias_action :manage_details, :download, to: :read
But upon doing this I receive
CanCan::Error
You can't specify target (read) as alias because it is real action name
The reason I decided to go with the above approach because I don't want to place
authorize! :read, #project in the controller action
However if I do
alias_action :read, :manage_details, :download, to: :view_manage_details
and give users access to view_manage_details it works fine. Is there something that can be done to achieve alias_action :manage_details, :download, to: :read or is it something that requires extra mile work?

Ruby on Rails - Why the code inside my controller is not called

I retrieved a Web API coded in Ruby on Rails.
I'm trying to be aware of the code of my predecessor, but fails to understand why my route does not lead to my controller.
My route is configured this way:
resources :bars, :defaults => { :format => 'json' } do
member do
post :subscription
delete :subscription
get :bar_comments
get :events
get :default_event
get :dates
post :store_sections
put :store_sections
get :store_sections, to: 'bars#store_sections_index'
post :tables
put :tables
get :tables, to: 'bars#tables_index'
post :traffic_breakpoint
get :traffic
get :entrance_traffic
put :auto_events
get :fake_counter
post :fake_counter
put :stripe_info
end
collection do
get :favorites
get :test, :defaults => { :format => 'json' }
end
end
I want to catch a get request on bars/favorites url.
Here is how the favorites function is defined on bars_controller
def favorites
logger.debug "FAVORITE BARS!!!#####################################################################"
if current_user.present?
#bars = Bar.average_join.filter_favorite(current_user)
render :index
else
render json: { error: { error_code: 5, error_message: 'You need to be authentificated' } }, status: :unprocessable_entity
end
end
When I call bars/favorites, here is what I see in terminal and logs:
Started GET "/bars/favorites" for 111.111.11.111 at 2017-08-25 22:00:52 +0200
Processing by BarsController#favorites as JSON
Parameters: {"bar"=>{}}
AccessToken Load (0.8ms) SELECT "access_tokens".* FROM "access_tokens" WHERE "access_tokens"."access_token" = 'ebeddb822e1f36848162818a585df3d0' LIMIT 1
User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1 [["id", 1]]
Completed 404 Not Found in 11ms (Views: 0.5ms | ActiveRecord: 1.3ms)
The route is found and the controller too. If I rename the favorites function in controller, I will get an error:
AbstractController::ActionNotFound (The action 'favorites' could not be found for BarsController):
app/helpers/sabayon_middleware.rb:25:in `call'
I can't understand why my log message (logger.debug "FAVORITE BARS!!!.....) doesn't appear on logs, it's like the favorite function was called but nothing of what is supposed to do happens (inside the function).
Plus I have exactly the same structure for another entity (clubs), and it's just working perfectly. Here is how the clubs route is defined:
resources :clubs, :defaults => { :format => 'json' } do
member do
post :subscription
delete :subscription
get :club_comments
get :events
get :default_event
get :dates
post :store_sections
put :store_sections
get :store_sections, to: 'clubs#store_sections_index'
post :tables
put :tables
get :tables, to: 'clubs#tables_index'
post :traffic_breakpoint
get :traffic
get :entrance_traffic
put :auto_events
get :fake_counter
post :fake_counter
put :stripe_info
end
collection do
get :favorites
end
end
And the favorite function in controller:
def favorites
logger.debug "FAVORITES CLUBS #####################################################################"
if current_user.present?
#clubs = Club.average_join.filter_favorite(current_user)
render :index
else
render json: {error: {error_code: 5, error_message: 'You need to be authentificated'}}, status: :unprocessable_entity
end
end
In this case everything works perfectly, when I call clubs/favorites I get:
Started GET "/clubs/favorites" for 111.111.11.111 at 2017-08-25 22:23:26 +0200
Processing by ClubsController#favorites as JSON
Parameters: {"club"=>{}}
AccessToken Load (0.7ms) SELECT "access_tokens".* FROM "access_tokens" WHERE "access_tokens"."access_token" = '5835db5eb54c0d05114ceb86688b2a31' LIMIT 1
User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1 [["id", 1]]
FAVORITES CLUBS #####################################################################
Club Load (2.4ms) SELECT clubs.*, round(AVG (club_comments.rate), 2) AS rate_average, clubs.*, club_subscriptions.id IS NOT NULL AS is_favorite FROM "clubs" LEFT OUTER JOIN club_comments ON club_comments.club_id = clubs.id INNER JOIN club_subscriptions ON club_subscriptions.club_id = clubs.id AND club_subscriptions.user_id = 1 GROUP BY clubs.id, club_subscriptions.id
Rendered clubs/index.json.jbuilder (4.7ms)
Completed 200 OK in 28ms (Views: 14.4ms | ActiveRecord: 5.5ms)
UPDATE 28.08.2017
I managed to fix the issue by commenting this line in bars_controller:
load_and_authorize_resource
So I don't understand why clubs/favorites is working and bars/favorites, but at least I can work now.
UPDATE 29.08.2017
As asked by #Leito, here is the content of my Ability Class:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
#can :read, :all
default_abilities
if user.persisted?
user_basic_abilities(user)
if user.admin?
club_owner_abilities(user)
can :manage, :all
elsif user.club_owner?
club_owner_abilities(user)
elsif user.user?
end
else
not_connected_abilities
end
end
def default_abilities
can [:index, :show, :events, :dates, :club_comments, :tables_index, :store_sections_index, :letsencrypt, :fake_counter], Club
can [:index, :show], Artist
can [:index, :show, :dates, :tables_index, :premium], Event
can [:index, :show], Ticket
end
def user_basic_abilities(user)
can [:index, :show, :update, :me, :update_password, :club_comments, :customer, :get_customer, :source, :select_source, :delete_source, :bookings], User, id: user.id
can :upload, Picture
can :destroy, Picture, user_id: user.id
can :create, Booking
can :share, Booking do |booking|
booking.owner.id == user.id
end
can :show, Booking do |booking|
booking.user_entrances.where(user_id: user.id).count > 0
end
can :subscription, Club
can :favorites, Club, user_id: user.id
can :create, ClubComment
can [:update, :destroy], ClubComment, user_id: user.id
can [:subscription, :vote], Artist
can [:favorites, :subscription], Event
can :create, Cart
can [:index, :update, :destroy, :cart_items, :user_infos, :order], Cart, user_id: user.id
can [:payment], Order do |order|
order.user.id == user.id
end
end
def club_owner_abilities(user)
can :create, Club
can :create, Artist
can :upload, Picture
can :destroy, Picture, user_id: user.id
can [:update, :tables, :store_sections, :traffic_breakpoint, :traffic, :entrance_traffic, :default_event, :auto_events], Club, id: user.club_id
can [:update, :stats, :votes], Artist
can :create, Event
can [:update, :destroy, :tables, :default_tables, :bookings], Event, club_id: user.club_id
can [:create, :update, :destroy], Promotion do |promo|
promo.event && promo.event.club_id == user.club_id
end
can [:receipt, :confirmation_table, :cancelation_table], Booking
can [:consume, :show, :payments, :refund_payment], Booking do |booking|
booking.event && booking.event.club_id == user.club_id
end
can [:create, :update, :destroy], Ticket do |ticket|
ticket.event && ticket.event.club_id == user.club_id
end
end
def not_connected_abilities
can :create, AccessToken
can [:create, :login], User
# can :register, MobileDevice
end
end
load_and_authorize_resource comes from cancan or cancancan. These are autorhization gems that limit the records a user has access too. Let's see how club/favorites and bar/favorites are authorized differently.
From your Ability class it appears that the user is missing an authorization on can :favorites, Bar similar to the existing:
can :favorites, Club, user_id: user.id
Depending on how you rescue form CanCan::AccessDenied this could explain the response from your controller.
404 is returned by rails when an active record query fails with ActiveRecord::RecordNotFound. So I say, either User or Bar or any other related find methods are raising an error (due to not being available in DB).

CanCan - How to allow users to update and delete only their own objects

I've started a Rails application with Devise and CanCan. I have users which has a one-to-many relationship to articles. I'm new to CanCan, here's what I'm planning to do:
Admin
can do any action on articles
Logged in user
can read and create articles
can edit and destroy his own articles
Guest user
can read articles
But I'm having trouble understanding the syntax of CanCan. I understand it would be something like this.
def initialize(user)
user ||= User.new
if user.admin?
can :manage, Article
else
can :read, Article
end
end
But this is just for the admin and guest user, I'm not sure how to differentiate a guest user from a logged in user because it creates a new User object when user is empty. I've seen that the code should be something like this can [:edit, :destroy], Article, :user_id => user.id, but I'm not sure how this would fit in the initialize method.
And one last question, if I only define a can :read, Article on guests, would it block the other actions such as create and update, like white listing the read action?
Any help would be appreciated. Thanks a lot!
Here's what I did:
In ability.rb
def initialize(user)
if user.nil?
can :read, Article
elsif user.admin?
can :manage, Article
else
can [:read, :create], Article
can [:update, :destroy], Article, :user_id => user.id
end
end
And for displaying the links, I've used this:
- if can? :read, Article
= link_to 'Show', article
- if can? :create, Article
= link_to 'New Article', new_article_path
- if can? :update, article
= link_to 'Edit', edit_article_path(article)
- if can? :destroy, article
= link_to 'Destroy', article, method: :delete, data: { confirm: 'Are you sure?' }
And it seems to be working now, not sure if that's the best way though.
You can pass hash of conditions:
can :manage, Article, :user_id => user.id
Look at https://github.com/ryanb/cancan/wiki/defining-abilities for details.

Cancan Ability Issue with Inheritance

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.

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

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

Resources