Rails 4: CanCanCan abilities with has_many :throug

2019-07-12 21:02发布

问题:

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.

  1. I had not added load_and_authorize_resource to the calendars and administrations controllers.

  2. 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?

回答1:

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'
  ...