Search code examples
ruby-on-railsauthorizationcancancancancan

User with a membership can read all data


The Setup

I'm using Rails 5.2 with the cancancan gem.

rails g scaffold User first_name email:uniq
rails g scaffold Organization name:uniq
rails g scaffold Role name
rails g scaffold Membership user:references organization:references role:refences

user.rb

class User < ApplicationRecord
  has_many :memberships
  has_many :roles, through: :memberships
  has_many :organizations, through: :memberships
end

membership.rb

class Membership < ApplicationRecord
  belongs_to :role
  belongs_to :organization
  belongs_to :user
end

organization.rb

class Organization < ApplicationRecord
  has_many :memberships
  has_many :users, through: :memberships
end

role.rb

class Role < ApplicationRecord
  has_many :memberships
  has_many :users, through: :memberships
end

seeds.rb

admin = Role.create(name: 'Admin')
user = Role.create(name: 'User')

abc = Organization.create(name: 'Abc Inc.')

bob = User.create(first_name: 'Bob')
alice = User.create(first_name: 'Alice')

Membership.create(role: user, company: abc, role: user)
Membership.create(role: admin, company: abc, role: admin)

The Task

An admin should be able to manage all users and memberships of the company he/she is admin for. A user can only read all users and memberships of that company.

Here is my take on a cancancan configuration:

ability.rb

class Ability
  include CanCan::Ability

  def initialize(user)
    user ||= User.new

    user_role = Role.find_by_name('User')
    admin_role = Role.find_by_name('Admin')

    organizations_with_user_role = Organization.includes(:memberships).
      where(memberships: {user_id: user.id, role_id: user_role.id})
    organizations_with_admin_role = Organization.includes(:memberships).
      where(memberships: {user_id: user.id, role_id: admin_role.id})

    can :read, Organization, organizations_with_user_role
    can :manage, Organization, organizations_with_admin_role
  end
end

Then I try to run this code in a view:

<% if can? :read, organization %><%= link_to 'Show', organization %><% end %>

This results with an error page which says:

The can? and cannot? call cannot be used with a raw sql 'can' definition. The checking code cannot be determined for :read #

I guess I'm tackling the problem from a totally wrong angle. How do I have to setup the ability.rb to solve this problem?


Solution

  • From the documentation:

    Almost anything that you can pass to a hash of conditions in Active Record will work here. The only exception is working with model ids. You can't pass in the model objects directly, you must pass in the ids.

    can :manage, Project, group: { id: user.group_ids }
    

    So try something like:

    can :read, Organization, id: organizations_with_user_role.pluck(:id)
    

    On a separate note, why are you using includes instead of joins? Your query can be simplified to (without the need for user_role = Role.find_by_name('User')):

    organizations_with_user_role = Organization.joins(memberships: :role).
      where(memberships: {user_id: user.id}).where(roles: {name: 'User'})