Search code examples
ruby-on-railscontrollerauthorizationcancancancancan

CanCan Rails How to Limit Users From Reading Data They Didn't Create


I'm using CanCan 1.6.1 in a Rails 3.2 application. I use authorize_resource in my controllers to limit what users can :read, :create, :update: and :delete.

In my V2Posts controller, the authorize_resource call isn't working for my :show action.

In my abilities file, I'm trying to limit the user to only read their own posts. There's no error, but unfortunately, they're able to view other users posts.

class V2PostsController < ApplicationController
  layout 'panel_layout', only: :show
  before_filter :redirect_if_existing, only: [:new, :create]

  authorize_resource

  def show
    @v2_post = V2Post.find(params[:id], include: :user_updates)
    @organization = @v2_post.organization

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

ability.rb:

class Ability
  include CanCan::Ability

  @user = nil

  def initialize(user)
    alias_action :taco,
                 :sandwich,
                 to: :read

    @user = user

    if user.is_in_role?(:admin)
      can :manage, :all
    else
      default_rules

      user.roles_list.each do |role|
        meth = :"#{role}_rules"
        send(meth) if respond_to? meth
      end
    end
  end

  def default_rules
    cannot [:read, :create, :update, :destroy], :all
    ...
  end

  def pro_user_rules
    can :read, V2Post, proid: @user.id
  end
...
end

models/v2_post.rb:

class V2Post < ActiveRecord::Base
  attr_accessible :proid, :user_updates_attributes, :organization

  belongs_to :user,  :foreign_key => "proid"
  has_many :user_updates, as: :pro_post, dependent: :destroy
  accepts_nested_attributes_for :user_updates

end

load_and_authorize_resource works for keeping the users from viewing other users' posts. But, it loads the resource for me and also adds additional database calls to other actions. Shouldn't authorize_resource work for my case? I've explicitly defined @v2_posts in the show action (loading the resource myself). Why isn't it getting passed to the authorize_resource call?

  • I want to avoid requiring extra calls to the database
  • I don't want to automatically load the resource for custom actions
  • I want to authorize every CRUD action.

Given that, which of load_and_authorize_resource and authorize_resource works better, and what are the benefits/drawbacks of each? I've read the documentation, I'm confused.


Solution

  • The issue is that authorize_resource is being run as a before filter, and the instance isn't being set until inside your action--after authorize_resource is called. When the instance is nil, the authorize_resource method defaults to authorizing the class V2Report. Authorizing the class ignores the attribute conditions (e.g. can :read V2Report, user_id: @v2_post.user_id).

    You can use load_and_authorize resource, and have CanCan set the instance variable in your controller before each action. For actions that load a resource in a more complicated way, like your V2Post.find(params[:id], include: :user_updates), you can create a custom prepend_before_filter method that loads the resource for certain actions. As long as that's prepended, that loaded instance will be passed to the authorize_resource method. The load_and_authorize_resource will not make an additional call to the database if the instance is already loaded.

    class V2PostsController < ApplicationController
      prepend_before_filter :custom_load_resource, only: [:show, :edit]
      load_and_authorize_resource
    
      def show
        render @v2_post
      end
    
      private
    
      def custom_load_resource
        @v2_post = V2Post.find(params[:id], include: :user_updates)
      end
    
    end
    

    You can also forget the load_resource altogether, and use your own before_filter 100% of the time. Or, you can ditch the filters and call authorize! from within the actions. In my recent experience, the approach detailed above works nicely for RESTful controllers.