ruby-on-railsauthorizationcancancancancanruby-paranoia

Rails 5 compatibility between Paranoia and CanCanCan, compromised?


I'm having the exact same issue as described on this thread:

Rails 5 only_deleted with cancancan #356

I can access a deleted record, like this:

    @area = Area.only_deleted.find(params[:id])

but if I add load_and_authorize_resource to my controller, it'll attempt to run the query like this:

    @area = Area.find(params[:id])

which will result in error since it won't find a record with that id on a collection where deleted_at isn't null (not deleted records, the purpose of the Paranoia gem).

If I disable load_and_authorize_resource for the controller or for that very action, it solves the error but it's not a solution since that means losing authorization control.

Is there a fix for this, or is there an authorization gem which plays nice with Paranoia on Rails 5 which I could switch over to?

Thank you.


Solution

  • So, according to documentation on load_and_authorize_resource, the method will attempt to load an instance variable in case one hasn't been set yet, and won't do so if there's a set instance variable, which is precisely why the application was breaking:

    class AreasController < ApplicationController
    
      load_and_authorize_resource
    
      before_action :set_area, only: [:show, :edit, :update, :destroy]
    
      ...     
    
      def set_area
        if session[:show_obsolete_records] == true
          @area = Area.only_deleted.find(params[:id])
        else
          @area = Area.find(params[:id])
        end
      end
    end
    

    load_and_authorize_resource runs first on the list, and since there were no instance variables set before its call, it does @area = Area.find(params[:id]) on its own account, which obviously leads to error, since Paranoia overwrittes finder methods to include a condition to check whether the deleted_at is NULL.

    For example, when using the regular (without Paranoia) Area.find(17), you get a query like this on your console:

    Area Load (0.2ms)  SELECT  "areas".* FROM "areas" WHERE "areas"."id" = ? LIMIT ?  [["id", 17], ["LIMIT", 1]]
    

    When using Paranoia, you'd get this query:

    Area Load (0.2ms)  SELECT  "areas".* FROM "areas" WHERE ("areas"."deleted_at" IS NULL) AND "areas"."id" = ? LIMIT ?  [["id", 17], ["LIMIT", 1]]
    

    This way, records that have been deleted won't be found on common queries since they'll have the deleted_at timestamp set (deleted_at is now NOT NULL).

    To access deleted records, you must use either with_deleted or only_deleted, like

    @area = Area.only_deleted.find(params[:id])

    or else it won't find the deleted record, hence why I was getting the error

    ActiveRecord::RecordNotFound - Couldn't find Area with 'id'=16 [WHERE "areas"."deleted_at" IS NULL]:
    

    The method load_and_authorize_resource loaded @area = Area.find(params[:id]) and skipped set_area, so you could delete the method and it would still set the area even if the code is not there.

    The solution is to simply move the load_and_authorize_resource method below the callbacks list:

    class AreasController < ApplicationController
    
      before_action :set_area, only: [:show, :edit, :update, :destroy]
    
      load_and_authorize_resource
    
      ...     
    
      def set_area
        if session[:show_obsolete_records] == true
          @area = Area.only_deleted.find(params[:id])
        else
          @area = Area.find(params[:id])
        end
      end
    end
    

    UPDATE

    You can leave the method call load_and_authorize_resource at the top at the stack, but change it to authorize_resource so it doesn't attempt to call @area = Area.find(params[:id]), according to this thread.

    class AreasController < ApplicationController
    
      authorize_resource
    
      before_action :set_area, only: [:show, :edit, :update, :destroy]
    
      ...     
    
      def set_area
        if session[:show_obsolete_records] == true
          @area = Area.only_deleted.find(params[:id])
        else
          @area = Area.find(params[:id])
        end
      end
    end