4

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
cdouble.bhuck
  • 507
  • 1
  • 5
  • 19
  • A couple of things first: the variable `@clients` inside the methods is actually a `@client`, right? (singular, not plural, please change that), second: this is loaded already via `load_and_authorize_resource`, so why do you load it again? Can you also post the rest of your ability.rb? thanks – coorasse Sep 08 '17 at 06:33
  • I actually noticed that going through the docs last night, I had already built most of the controllers and such before I implemented CanCan and I guess I never changed it, but have done so now. I went through all my views and changed `@clients` to `@client` and have posted my updated `external/clients_controller.rb` as well as my full `ability.rb`. Thank you for helping me out. – cdouble.bhuck Sep 08 '17 at 15:21
  • You have a rule which states that a user `can :read, :all` if is not an admin. this will allow all non-admin users to read all models. – coorasse Sep 09 '17 at 09:28
  • Yikes, I guess I really don't understand CanCan because I thought the call to `user.has_role? :client` would override that. Thank you for taking the time to be another set of eyes. Everything works as expected now. If you feel like making your comment into an answer then I'll will accept it for you. – cdouble.bhuck Sep 09 '17 at 15:00

1 Answers1

2

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.

coorasse
  • 5,278
  • 1
  • 34
  • 45
  • Ahhh I like that idea, I semi-grasped the concept before but putting `can... ` then `cannot.. ` afterwards but I thought it was contained in each `user.has_role?` block so thank you for clearing that up and for the suggestions. – cdouble.bhuck Sep 09 '17 at 18:43