CanCan index action ability - ruby-on-rails

I'm having some trouble defining permissions for my albums#index action. The path to it is /user/:user_id/albums - this is the ability for my :show action (:read => [:index, :show]) which is working well. The path to it is /user/:user_id/albums/:id/.
can :read, Album do |album|
#user.friends_with?(album.user_id)
end
I'm not sure how to write a similar rule for the index action, or if I even want to use CanCan here. The rule is:
current_user MUST be .friends_with?(user_id) to view any albums belonging to user_id.
user_id is of course taken from params[:user_id]. Note: /user/eml/albums/ would be the path, I'm not fetching users by their .id but by their .username!
class Ability
include CanCan::Ability
def initialize(user)
#user = user || User.new # for guest, probably not needed
#user.roles.each { |role| send(role) }
end
def user
can :read, Album do |album|
#user.friends_with?(album.user_id)
end
can :manage, Album do |album|
album.user_id == #user.id
end
end
end
UPDATE:
Turns out the solution is really simple, I was just not paying attention to my routes:
resources users do
resources albums
end
In the controller that becomes pretty easy then:
load_and_authorize_resource :user, :find_by => :username
load_and_authorize_resource :album, :through => :user
And the rule:
can :read, Album, :user_id => #user.friend_ids # I don't need #user.id
I'm not perfectly happy with it though, as using the user.friends_with?(other_user) method would be much quicker and doesn't have to fetch (potentially) a thousand ids from the database. Any other solution is most welcome.

On IRC you told me that the .roles_mask isn't important... then shouldn't this be something like:
class Ability
include CanCan::Ability
def initialize(user)
if user
can :read, Album, :user_id => [user.id] + user.friend_ids
can :manage, Album, :user_id => user.id
end
end
end

Related

Rails 4: CanCanCan abilities with has_many :through association

I have a Rails app with the following models:
class User < ActiveRecord::Base
has_many :administrations
has_many :calendars, through: :administrations
end
class Calendar < ActiveRecord::Base
has_many :administrations
has_many :users, through: :administrations
end
class Administration < ActiveRecord::Base
belongs_to :user
belongs_to :calendar
end
For a given calendar, a user has a role, which is define in the administration join model.
For each calendar, a user can have only one of the following three roles: Owner, Editor or Viewer.
These roles are currently not stored in dictionary or a constant, and are only assigned to an administration as strings ("Ower", "Editor", "Viewer") through different methods.
Authentication on the User model is handled through Devise, and the current_user method is working.
In order to only allow logged-in users to access in-app resources, I have already add the before_action :authenticate_user! method in the calendars and administrations controllers.
Now, I need to implement a role-based authorization system, so I just installed the CanCanCan gem.
Here is what I want to achieve:
All (logged-in) users can create new calendars.
If a user is the owner of a calendar, then he can manage the calendar and all the administrations that belong to this calendar, including his own administration.
If a user is editor of a calendar, then he can read and update this calendar, and destroy his administration.
If a user is viewer of a calendar, then he can read this calendar, and destroy his administration.
To implement the above, I have come up with the following ability.rb file:
class Ability
include CanCan::Ability
def initialize(user, calendar)
user ||= User.new
calendar = Calendar.find(params[:id])
user can :create, :calendar
if user.role?(:owner)
can :manage, :calendar, :user_id => user.id
can :manage, :administration, :user_id => user.id
can :manage, :administration, :calendar_id => calendar.id
elsif user.role?(:editor)
can [:read, :update], :calendar, :user_id => user.id
can :destroy, :administration, :user_id => user.id
elsif user.role?(:viewer)
can [:read], :calendar, :user_id => user.id
can :destroy, :administration, :user_id => user.id
end
end
end
Since I am not very experimented with Rails and it is the first time I am working with CanCanCan, I am not very confident with my code and would like some validation or advice for improvement.
So, would this code work, and would it allow me to achieve what I need?
UPDATE: with the current code, when I log in as a user, and visit the calendars#show page of another user's calendar, I can actually access the calendar, which I should not.
So, obviously, my code is not working.
Any idea of what I am doing wrong?
UPDATE 2: I figured there were errors in my code, since I was using :model instead of Model to allow users to perform actions on a given model.
However, the code is still not working.
Any idea of what could be wrong here?
UPDATE 3: could the issue be caused by the fact that I use if user.role?(:owner) to check if a user's role is set to owner, while in the database the role is actually defined as "Owner" (as a string)?
UPDATE 4: I kept on doing some research and I realized I had done two mistakes.
I had not added load_and_authorize_resource to the calendars and administrations controllers.
I had defined two attributes two parameters — initialize(user, calendar) — instead of one in my initialize method.
So, updated both controllers, as well as the ability.rb file as follows:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new
if user.role?(:owner)
can :manage, Calendar, :user_id => user.id
can :manage, Administration, :user_id => user.id
can :manage, Administration, :calendar_id => calendar.id
elsif user.role?(:editor)
can [:read, :update], Calendar, :user_id => user.id
can :destroy, Administration, :user_id => user.id
elsif user.role?(:viewer)
can [:read], Calendar, :user_id => user.id
can :destroy, Administration, :user_id => user.id
end
end
end
Now, when I try to visit a calendar that does not belong to the current_user, I get the following error:
NoMethodError in CalendarsController#show
undefined method `role?' for #<User:0x007fd003dff860>
def initialize(user)
user ||= User.new
if user.role?(:owner)
can :manage, Calendar, :user_id => user.id
can :manage, Administration, :user_id => user.id
can :manage, Administration, :calendar_id => calendar.id
How I can fix this?
There is no such method role? the User model. The Cancancan documentation is at fault for assuming such a method exists in the examples.
To fix this, you should instead do:
if user.role == 'Owner'
...
elsif user.role == 'Editor'
...
elsif user.role == 'Viewer'
...

Cancan :create ability

I have the following code:
#/app/models/users/user.rb
class Users::User < ActiveRecord::Base
has_many :phones, class_name: "Users::Phone"
end
#/app/models/users/phone.rb
class Users::Phone < ActiveRecord::Base
belongs_to :user, class_name: "Users::User"
attr_accessible :phone
end
#/app/models/ability.rb
class Ability
include CanCan::Ability
def initialize(user)
can :read, :all
unless user.nil? #logged_in
if user.is? :admin
can :manage, :all
else
can :create, Users::Phone, user_id: user.id
end
end
end
end
I wanna check ability for create only their own phones for users
#/app/views/users/users/show.html.slim
- if can? :create, Users::Phone.new
a[href="#{new_user_phone_path(#user)}"] Add phone
Thats does not work, because I should pass user_id to phone model (like Users::Phone.new user_id: user.id), but I can't do that since Phone's mass assignment.
So how I can check :create phones ability for users?
I do something similar to this in my app by making Ability aware of the underlying parameter structure. You have a few options depending on your requirements. So in your controller you'd have approximately:
def create
#phone = Users::Phone.new(params[:users_phone])
# Optional - this just forces the current user to only make phones
# for themselves. If you want to let users make phones for
# *certain* others, omit this.
#phone.user = current_user
authorize! :create, #phone
...
end
then in your ability.rb:
unless user.nil? #logged_in
if user.is? :admin
can :manage, :all
else
can :create, Users::Phone do |phone|
# This again forces the user to only make phones for themselves.
# If you had group-membership logic, it would go here.
if phone.user == user
true
else
false
end
end
end
end

rails and cancan - how to restrict the company show action to users owner in has_many through association

i'm trying to solve an user's ability problem with cancan gem.
company and users are associated through user_company_assignment in such a way that a company has many user and the users has and belongs to many companies
I would like to restrict the show action of a company only to those users associated with the company. below there is the code of the two models and a snip of ability.rb with the initialize role inheritance and the method for the seller user, but this is not working, it show me always the company detail.
Company.rb
has_many :user_company_assignments
has_many :user, :through => :user_company_assignments
User.rb
has_many :user_company_assignments
has_many :companies, :through => :user_company_assignments
Ability.rb
def initialize(user)
#user = user || User.new # for guest
#user.roles.each { |role| send(role.name.downcase) }
end
def seller
can :manage, :all
cannot :destroy, :all
can :show, Company do |company|
company.user_ids.include? #user.id
end
end
Your error is due to ability precedence: https://github.com/ryanb/cancan/wiki/Ability-Precedence
This line overrides all following abilities: can :manage, :all
Since you've already stated that a seller can manage all, the seller can perform any kind of action on a Company, regardless of the other can statement.
One solution would be to use cannot, as you did with :destroy. It will override the :manage, :all clause.
def seller
can :manage, :all
cannot :destroy, :all
cannot :show, Company do |company|
!company.user_ids.include? #user.id
end
end
You need to call the load_and_authorize_resource method in your controller.
Ok jesper, i have changed my ability.rb and it works but i'm not sure that this is the best method to set the ability, it is strange the i need to specify each Models that a seller has the permission to the show action. tell me if is it the best way to do that:
Ability.rb
def seller
can [:index, :create], :all
cannot :destroy, :all
can :show, Company do |company|
company.user_ids.include? #user.id
end
can :show, [Report, Client]
end

Cancan user permission?

I am using cancan for permissions and I would like it so users cannot see other users when visiting their profile/users page. User should only be able to see themselves.
In my ability.rb file I have
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
if user.role? :admin
can :manage, :all
elsif user.role? :rookie
can [:update, :destroy], [Album, Photo, User]
can :read, :all
end
can :manage, Album, :profile => { :user_id => user.id }
can :manage, Photo, :profile => { :user_id => user.id }
can :manage, Video, :profile => { :user_id => user.id }
can :manage, Comment, :blog => { :profile => { :user_id => user.id } }
can :manage, User, :id => user.id
end
end
In my users_controller I have
class UsersController < ApplicationController
before_filter :authenticate_user!
load_and_authorize_resource
def index
#user = current_user
end
def show
#user = User.find(params[:id])
end
end
The above usually works but since user is the primary model I am not sure how I can resolve this. Rails gives me the error
undefined method `user_id'
It should be:
#ability.rb:
can :manage, User, :id => user.id
I am assuming that most users start with role :rookie and you have ability
can :read, :all
for users with role :rookie. Then this means that all :rookie users will be able to read
all resources.

Rails cancan authorizing nested resources

I have Projects resource which is nested in Users resource.
My Cancan Ability class is:
class Ability
include CanCan::Ability
def initialize(user)
#everyone
can :read, Project
if user.blank?
# guest user
...
else
#every signed in user
case user.role
when User::ROLES[:admin]
#only admin role user
can :manage, :all
when User::ROLES[:member]
#only member role user
can :update, User, :id => user.id
can [:create, :update, :destroy], Project, :user_id => user.id
else
end
end
end
end
And Projects controller:
class ProjectsController < ApplicationController
load_and_authorize_resource :user
load_and_authorize_resource :projects, :through => :user, :shallow => true
...
end
I have few questions:
Is it possible to deny :read User and allow to :read Project, so that everyone could access /users/10/projects, but not /users/10 or /users?
How can I deny user accessing :new action with other user_id? For example, if I add
#everyone
can :read, User
can :read, Project
this code allows user with id 42 to access /user/41/projects/new.
Solved it by doing:
class Ability
include CanCan::Ability
def initialize(user)
#everyone
can :read, Project
can :read, User # required to access nested resources
cannot :index, User
cannot :show, User
if user.blank?
# guest user
...
else
#every signed in user
case user.role
when User::ROLES[:admin]
#only admin role user
can :manage, :all
when User::ROLES[:member]
#only member role user
can :update, User, :id => user.id
can :manage, Project, :user => { :id => user.id }
else
end
end
end
end

Resources