Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

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

Open
kibaekr opened this Issue · 9 comments

3 participants

@kibaekr

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 }
  end
end

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

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

in model/ability.rb we have

class Ability
include CanCan::Ability

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

  if user.has_role? :reader 
     reader(user)
  end

  if user.has_role? :writer
    writer(user)
  end
end

#BE ABLE TO SEE COURSES AND CATS FOR PERMITTED CATS.
def reader(user)
  can :read, Category, :roles => { :user_id => user.id, :level => "reader" }
   ## how would we be able to limit reading of courses that are within permitted categories? something like category.courses ~~ 
end

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

Questions:

  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

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

@graywh

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 => user.id
end
@kibaekr

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

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

can :read, Category do |category|
  user.roles.where(category_id: category.id).map(&:level).include? "reader"
end

can :manage, Category do |category|
  user.roles.where(category_id: category.id).map(&:level).include? "writer"
end

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

@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

@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

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

@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
end
@graywh

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