Search code examples
ruby-on-railscancancancancan

cancancan Abilities with has_and_belongs_to_many


I have the following relationship between my classes user and page.

class User < ApplicationRecord
  has_and_belongs_to_many :pages
end

class Page < ApplicationRecord
  has_and_belongs_to_many :users
end

How can I do in my Ability file for a user to only edit the pages that belong to him?

class Ability
  include CanCan::Ability
  def initialize(user)
      if user.is? :page_administrator
        can :manage, Page
      end
  end
end

I try the following, but I still can not.

  can :manage, Page, users: { user_id: user.id }

Solution

  • The biggest con with has_and_belongs_to_many is that it makes it impossible to attach data to the join table. Instead use a join model:

    class User < ApplicationRecord
      has_many :user_pages
      has_many :pages, through: :user_pages
    end
    
    class Page < ApplicationRecord
      has_many :user_pages
      has_many :users, through: :user_pages
    end
    
    class UserPage < ApplicationRecord
      belongs_to :user
      belongs_to :page
    end
    

    This works like has_and_belongs_to_many except its not headless - you can query UserPage directly. The only thing you need to do besides creating the UserPage model is renaming the table from users_pages to user_pages (or pages_users to page_users).

    class RenameUsersPages < ActiveRecord::Migration[5.0]
      def change
        rename_table('users_pages', 'user_pages')
      end
    end
    

    This is needed since rails will link the table to the constant Users::Page otherwise.

    Now you can easily attach a admin flag on the UserPage table.

    class AddPageAdministratorToUserPages < ActiveRecord::Migration[5.0]
      change_table :users do |t|
        t.boolean :admin, default: false
       end
    end
    

    Now we can check if a user is an admin by checking if a record exists in user_pages:

    class Ability
      include CanCan::Ability
      def initialize(user)
          can :manage, Page do |p|
            p.user_pages.exists?(admin: true, user: user)
          end
      end
    end