Search code examples
ruby-on-railsrubyrubygemsauthorizationcancan

Cancan Authorization Forum


I'm working on a project using RoR, a social message board (internet forum), in which every user can create multiple Boards and join multiple Boards from other users.

I didn't want to reinvent the wheel so I'm using Devise for Authentication and CanCan for Authorization. However I'm having some issues implementing CanCan because of the following:

class Board < ActiveRecord::Base
  has_many :memberships
  has_many :users , :through => :memberships
end

class User < ActiveRecord::Base
  has_many :memberships
  has_many :boards, :through => :memberships
end

class Membership < ActiveRecord::Base
  ROLE = ['Administrator', 'Moderator','Member', 'Banned']
  belongs_to :user
  belongs_to :board
end

The role doesn't belong to the user himself, it belongs to the relationship between the user and the board, that is the Membership. So it's not enough knowing who is the current_user I also need to know which board is being displayed, so I think I would have to send the Membership instead of the user to the Ability class initializer? Any guidance would be greatly appreciated.


Solution

  • You're on the right path.

    If you haven't already, create this as an entirely new Ability. e.g. BoardAbility. I've found it useful to not be shy about passing-in additional dependencies, and to have CanCan do as much of the evaluation that's reasonable.

    class BoardAbility
      include CanCan::Ability
    
      attr_reader :requested_by, :requested_resource
    
      def initialize requested_by, requested_resource
        return nil unless (requested_by.is_a?(User) && requested_resource.is_a?(Board))
    
        @requested_by       = requested_by
        @requested_resource = requested_resource
    
        default_rules
      end
    
      private
    
      def default_rules
        # common abilities to all users
        can :flag_offensive,    :all
        can :view_thread_count, :all
    
        # find this user's role to this board to define more abilities
        role = Membership.where(user_id: requested_by.id, board_id: requested_resource.id).pluck(:role).first
    
        if ['Administrator', 'Moderator'].include? role
          can :ban_users, Board, {id: requested_resource.id}
        end
      end
    end
    

    Then in your BoardController define a private method to signify that we aren't using the default CanCan Ability class.

    def current_ability
      @current_ability ||= BoardAbility.new(current_user, @board)
    end
    

    Then when you're in your BoardController, use the usual CanCan DSL.

    authorize! :ban_user, @board