Search code examples
ruby-on-railsruby-on-rails-5cancancancancanrolify

CanCanCan show only instance of model that user belongs to


I'm currently using Devise, CanCan, and Rolify to handle authentication and authorization for my rails app and I'm having a really hard time understanding how to make it so that a user can only :show and :update a specific instance of a model that the user belongs to (aka my user has a client_id column, not the other way around).

The update part of my defined Abilities.rb for the user with the :client role, works fine, i.e. if current_user.client_id = 3 then he can only update a client where Client.id = 3, however, that same user can SEE any instance of the Client model and I can't seem to grasp how to limit this.

Ability.rb

...
if user.has_role? :client
  can [:read, :update], [Property, Order], client_id: user.client_id
  can [:read, :update], Owner
  can :create, [Property, Order, Owner]
  can :manage, User, id: user.id
  can [:show, :update], Client, id: user.client_id
end
...

Each user does not have an index of all Clients, so after researching I changed can [:read, :update], Client, .. to :show but the users can still see the other clients but the :update part if it works fine, so I'm really at a loss here. Have been googling for the past few hours and read through all the CanCan documentation of which I acknowledge that it maybe addressed but I can't figure it out.

I've tried limiting it from the controller side as shown below but that doesn't work either:

external/clients_controller.rb

class External::ClientsController < ApplicationController
  load_and_authorize_resource
  before_filter :client_only

  def index
    @clients = Client.paginate(page: params[:page], per_page: 15)
  end

  def show
    @clients = Client.find(params[:id])
    @client_users = User.where(client_id: params[:id])
    @client_orders = Order.where(client_id: params[:id]).includes(:property, :owners)
    can? :show, @clients
  end

  def edit
    @clients = Client.find(params[:id])
    respond_to do |format|
      format.html { @clients.save }
      format.js
    end
  end

  def update
    @clients = Client.find(params[:id])
    @clients.update_attributes(client_params)
    respond_to do |format|
      format.html { if @clients.save
                      flash[:success] = "Client Updated Successfully"
                      redirect_to client_path(@clients)
                    else
                      render 'edit'
                    end
      }
      format.js
    end
  end

  private

  def client_params
    params.require(:client).permit(:uuid, :company, :first_name, :last_name, :phone, :email, :address1, :address2, :city, :state, :zip, :notes)
  end

  def client_only
    redirect_to root_path unless current_user.is_client?
  end

end

So if anyone could help me fully understand how CanCan handles role based authorization for an instance of a model then I would greatly appreciate it. Thanks in advance!

Updated Code

Removed all @client instance loads in external/clients_controller.rb

class External::ClientsController < ApplicationController
  load_and_authorize_resource
  before_filter :client_only

  def show
    @client_users = User.where(client_id: params[:id])
    @client_orders = Order.where(client_id: params[:id]).includes(:property, :owners).paginate(page: params[:page], per_page: 15).order("order_number DESC")
  end

  def edit
    respond_to do |format|
      format.html
      format.js
    end
  end

  def update
    if params[:client][:state].blank?
      params[:client][:state] = @client.try(:state)
    end
    @client.update_attributes(client_params)
    respond_to do |format|
      format.html { if @client.save
                      flash[:success] = "Client Updated Successfully"
                      redirect_to external_client_path(@client)
                    else
                      render 'edit'
                    end
      }
      format.js
    end
  end

  private

  def client_params
    params.require(:client).permit(:uuid, :company, :first_name, :last_name, :phone, :email, :address1, :address2, :city, :state, :zip, :notes)
  end

  def client_only
    redirect_to root_path unless current_user.is_client?
  end

end

full ability.rb

class Ability
  include CanCan::Ability

  def initialize(user)  
    alias_action :show, :to => :view
    alias_action :open_external_orders, :completed_external_orders, :to => :client_order_views

    user ||= User.new
    if user.has_role? :admin
      can :manage, :all
      can :assign_roles, User
    else
      can :read, :all
    end

    if user.has_role? :executive
      can :manage, [Property, Deed, Mortgage, Order, Owner, Client, AttachedAsset, User]
      cannot :assign_roles, User
    end

    if user.has_role? :management
      can :manage, [Property, Deed, Mortgage, Order, Owner, Client, AttachedAsset]
      can :read, User
      can :manage, User, id: user.id
      cannot :destroy, [Property, Order, Client, User]
    end

    if user.has_role? :analyst
      can :manage, [Property, Deed, Mortgage, Order, Owner, Client, AttachedAsset]
      can :manage, User, id: user.id
      cannot :destroy, [Property, Order, Client, User]
    end

    if user.has_role? :it
      can :manage, [Property, Deed, Mortgage, Order, Owner, Client, AttachedAsset]
      can :manage, User, id: user.id
      can :read, User
      cannot :destroy, [Property, Order, Client, User]
    end

    if user.has_role? :client
      can [:read, :update], Client, id: user.client_id
      can [:read, :update, :client_order_views], [Property, Order], client_id: user.client_id
      can [:read, :update], Owner
      can :create, [Property, Order, Owner]
      can :manage, User, id: user.id
    end
  end
end

Solution

  • CanCanCan works with "increasing permissions". Every rule can increase the previous one.

    If you write:

    can :show, User
    can :edit, User
    

    the two permissions will be joined and you'll be able to show and edit a User.

    In your ability.rb you are defining can :read, :all you grant permissions to read (show and index) on all objects.

    I suggest you to write your ability file according to the concept of "increasing permisssions". This means that you don't start defining the ability for the admin, but you do that at the end, adding the abilities an admin needs to the ones you gave to everyone already.