Skip to content

How to define multiple roles for each instance of a particular model? #927

kibaekr opened this Issue Aug 28, 2013 · 9 comments

3 participants

kibaekr commented Aug 28, 2013

I'm currently stuck on how to separate roles for CanCan depending on each condition that we want.
In our application, there are many categories (such as math, english, history, etc.) and within each are many courses.

Each user can have many different roles on each category. For example, John can be a "reader" for math, which means he can read all the courses that are in math. John can also be a "writer" for english, which means he can read all the courses in english, create a course within category english, and edit/delete only the courses he created within english.

If these were the only roles John had, he would not be able to see the category history in the navbar, and would be denied access to courses that are within history.

These are how relations are set up:

class User < ActiveRecord::Base
  has_many :roles

  def has_role?(role_sym)
    roles.any? { |r| r.level.underscore.to_sym == role_sym }

class Category < ActiveRecord::Base
  has_many :roles
  has_many :courses

class Role < ActiveRecord::Base
  belongs_to :user
  belongs_to :category
  attr_accessible :level, :category_id, :user_id

in model/ability.rb we have

class Ability
include CanCan::Ability

def initialize(user)
  user ||= # guest user (not logged in)  #guest

  if user.has_role? :reader 

  if user.has_role? :writer

def reader(user)
  can :read, Category, :roles => { :user_id =>, :level => "reader" }
   ## how would we be able to limit reading of courses that are within permitted categories? something like ~~ 

def writer(user)
  reader(user) #inheriting from reader? this doesnt work because level is hardcoded into reader
  can :read, Category, :roles => { :user_id =>, :level => "writer"}
  # can read all courses in category that you are given permission to
  # can write courses in permitted category
  # can edit, delete courses that only youve created within permitted category


  1. How do we separate the roles of "reader" and "writer" in the correct way? How do we access the courses that are within the categories that we have access to?

  2. After defining the reader and writer methods in the ability.rb, how do we use them in our view pages? It looks like the current documentations use something like "<% if can? :read, @category %>
    " but that doesn't use the methods we separated and defined.

p.s. We will have 7 different roles: guest, reader, writer, editor, manager, admin, and app_admin(our developers)

I've been trying to solve this for 3 days now - please understand that I'm still fairly a beginner! Thanks in advance

graywh commented Aug 28, 2013

The problem is that User#has_role? is too generic. Instead of using that, iterate over the user's roles in Ability#initialize.

graywh commented Aug 28, 2013

Maybe this will help:

user.roles.each do |role|
  read_categories = roles.where(:level => 'reader').pluck(:id)
  can :read, Category, :id => read_categories
  can :read, Course, :category_id => read_categories
  write_categories = roles.where(:level => 'writer').pluck(:id)
  can :read, Category, :id => write_categories
  can :create, Course, :category_id => write_categories  ## I don't think CanCan can handle this ##
  can [:update, :delete], Course, :user_id =>
kibaekr commented Aug 29, 2013

Thank you for the reply @graywh. You mentioned that it might be because the 'has_role?' function is too generic. Do you think it would make more sense to bring in two arguments, such as 'has_role?(role_sym, level)'?

Also, within the loop, should 'roles' be 'role'? And should 'pluck(:id)' be 'pluck(:category_id)'

zamith commented Sep 3, 2013

@kibaekr If I understand correctly you can solve it with:

can :read, Category do |category|
  user.roles.where(category_id: "reader"

can :manage, Category do |category|
  user.roles.where(category_id: "writer"

The category will be passed to the block in runtime, so you can look up only the category you need.

What I've noticed is that Role represents a many to many relation between User and Category, you should think of using a has many through relation.

Moreover, you should not allow for mass assignment of foreign keys.

As far as using it from the view, you call it like this:

<%= can? :read, @category %>

And that's the category that will be passed into the block.

Hope it helps.

kibaekr commented Sep 5, 2013

@zamith Thank you for the response. What is the main difference between your method and the one @graywh provided?

Also, what would be the reason for using a has many through relation? That would mean I could use "user.categories" and "category.users" but both don't show what sort of role the user has for the categories. It would only be useful to see what categories users have any sort of permission/role on - is that correct?

zamith commented Sep 6, 2013

@kibaekr Mine is more generic and only runs for the category you asking him to, not all of them.

I thought that you had a HABTM relation, and it would be useful. If not, then don't do it.

kibaekr commented Sep 12, 2013

Thanks again @zamith ! One more question if that's okay:

For this part "can :create, Course, :category_id => write_categories ## I don't think CanCan can handle this ##" that graywh wrote, I don't think it's working because there is no :category_id before a Course is made.

What would be some possible solutions to this? I want to restrict creating Courses to within the categories they are given "writer" access to.

zamith commented Sep 12, 2013

@kibaekr I think it doesn't work because :category_id is supposed to be checked against an id, not an array. you can do:

can :create, Course do |course|
  write_categories.include? course.category_id
graywh commented Sep 12, 2013

@zamith Yeah, that's it. The block syntax is no good for collection actions (e.g., loading multiple records for an index page). But it's fine for singular actions like create. For some reason, I've unconsciously avoided the block syntax at all costs even though it would have been easier. I'm off to update one of my applications now....

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.