ruby-on-railsrubycancan

Assign Conflicting CanCan Roles


I am using the CanCan Gem and would like to assign a User two roles which conflict. I need a User to have two roles, one is the x role and the other is the y role. Role x allows users to create posts but not create articles. Role y allows users to create articles but not create posts.

My ability.rb file is like the following:

if user.role?(x)
  can :manage, Post
  cannot :manage, Article
end
if user.role?(y)
 cannot :manage, Post
 can :manage, Article
end

Currently if I assign a User both roles, the permissions for role Y will override the permissions for role x. I would like to allow Admins to stack roles so that if I want to add roles x and y to a User, that User will be able to manage Posts and Articles. So if the role has a can and another role has a cannot, the can permission overrides.


Solution

  • Shouldn't

    if user.role?(x)
      can :manage, Post
    end
    if user.role?(y)
      can :manage, Article
    end
    

    be enough? cannot just removes previously granted permissions. But if you do not grant them, then you do not need to remove them.

    See also https://github.com/CanCanCommunity/cancancan/wiki/Ability-Precedence

    Here is a running example:

    require 'cancan'
    
    class Ability
      include CanCan::Ability
      attr_reader :user
    
      def initialize(user)
        @user = user
    
        if user.role?(:editor)
          can :edit, Post
        end
    
        if user.role?(:reader)
          can :read, Post
        end
      end
    end
    
    class Post
    end
    
    class User
      attr_reader :name
      def initialize(name, *roles)
        @name = name
        @roles = roles
      end
    
      def role?(role)
        @roles.include?(role)
      end
    end
    
    admin = User.new('admin', :reader, :editor)
    user = User.new('user', :reader)
    editor = User.new('editor', :editor)
    
    admin_ability = Ability.new(admin)
    user_ability = Ability.new(user)
    editor_ability = Ability.new(editor)
    
    
    def output(ability, permission)
      puts "#{ability.user.name} can #{permission} Post: #{ability.can?(permission, Post)}"
    end
    
    output(admin_ability, :edit)
    output(user_ability, :edit)
    output(editor_ability, :edit)
    puts "--"
    output(admin_ability, :read)
    output(user_ability, :read)
    output(editor_ability, :read)
    

    The default is to NOT HAVE a permission. So if you only work in "additive" mode where you add permissions if a user has roles, you will never need to remove one of them.

    :manage is the same as [:create, :edit, :update, :new, :destroy, :index, :show] and is only there for convenience. If you only want to add 6 of those permissions, you can add all 7 and then remove 1. But other than that I don't think you'll need cannot for your example.