Rails 5 compatibility between Paranoia and CanCanC

2019-07-28 23:05发布

问题:

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.

回答1:

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