4
votes

I'm using Rails 4.0.0, ruby 2.0.0p247, and CanCan 1.6.10.

How do I authorize users with CanCan based on their role in a join table (has_many :through)?

I have 3 models: User, Group, and GroupUser.

Users and Groups are associated as has_many through the GroupUser table. Each GroupUser also has a role field, which can be 'editor' or 'owner.' One group can have multiple Users, each with different roles. Also, a User can have roles in multiple Groups.

I have the app setup with CanCan abilities, but instead of limiting access to only users with the correct role, it is authorizing everybody.

The models are setup as follows. Also, notice that Group has a method to return a list of its owners.

class User < ActiveRecord::Base
  has_many :group_users
  has_many :groups, through: :group_users
end

class Group < ActiveRecord::Base
  has_many :group_users
  has_many :users, through: :group_users

  def owners
    User.find(self.group_users.where(role: 'owner').map(&:user_id))
  end
end

class GroupUser < ActiveRecord::Base
  belongs_to :user
  belongs_to :group
end

The CanCan ability. Notice that it uses the owners method on Group.

class Ability
  include CanCan::Ability

  def initialize(user)

    can :update, Group do |group|
      group.owners.include?(user)
    end

  end
end

The View is as follows. Here, users who shouldn't be able to see the link are still seeing it.

<ul class="groups">
    <% @groups.each do |group| %>
        <li>
            <p><%= group.name %></p>
            <% if can? :update, Group %>
                <%= link_to "Edit", edit_group_path(group) %>
            <% end %>
        </li>
    <% end %>
</ul>

Finally, the Controller action for the view is:

def index
  @groups = current_user.groups
end

. . . One interesting thing is that even though it doesn't work during actual use, the following unit test passes:

test 'user can only update groups they own' do
  ability_one = Ability.new(users(:one)) # Owner
  ability_two = Ability.new(users(:two)) # Not-owner

  assert ability_one.can?(:update, groups(:one))
  assert ability_two.cannot?(:update, groups(:two))
end
1

1 Answers

3
votes

I think you should make cancan work with a real object here, passing the actual group object to the can? method instead of the Group class:

In the view:

<% if can? :update, group %>

Instead of:

<% if can? :update, Group %>

This way you will ask cancan if current_user is able to :update the actual group. Since no ability has been set for operations on the generic Group object, the second condition will always be true.