问题
I use pundit to handle my API policies, I have an item show that can be forbidden to user in some cases, and in other cases just restricted. By restricted I mean it's forbidden now, but if he pays he could access it then. So I need my API to respond with a specific code (402 Payment Required
) so the client can invite the user to pay in order to unlock the show.
This is my current code, it only respond with 403
when pundit returns false.
Where would it be best to implement a condition to return 403
OR 402
in order to be DRY and clean?
class Api::V1::ItemController < Api::V1::BaseController
def show
@item = Item.find(params[:id])
authorize @item
end
end
class ItemPolicy < ApplicationPolicy
def show?
return true if record.public?
# 403 will be generated, that's ok.
return false if !record.band.members.include?(user)
# If that condition is false I want to generate a 402 error at the end, not a 403.
user.premium?
end
end
class Api::V1::BaseController < ActionController::API
include Pundit
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
def user_not_authorized(_exception)
# Here I've got the exception with :policy, :record and :query,
# also I can access :current_user so I could go for a condition,
# but that would include duplicated code from ItemPolicy#show?.
render json: { error: { message: "Access denied" } }, status: :forbidden
end
end
回答1:
Unfortunately Pundit
cannot handle different error types out of the box. And it is built to always expect the policy's methods to return true
or false false
. Therefore raising another custom error and rescueing from that in the controller will not work, because it would break view methods too.
I suggest a workaround to introduce different errors types. Something like this might work:
# in the policy
class ItemPolicy < ApplicationPolicy
def show?
return true if record.public?
return false unless record.band.members.include?(user)
if user.premium?
true
else
Current.specific_response_error_code = :payment_required
false
end
end
end
# in the controller
class Api::V1::BaseController < ActionController::API
include Pundit
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
def user_not_authorized(_exception)
case Current.specific_response_error_code
when :payment_required
render json: { error: { message: "Premium required" } }, status: :payment_required
else
render json: { error: { message: "Access denied" } }, status: :forbidden
end
end
end
I would not consider using the global CurrentAttributes
a good practice but they are part of Rails and in this case using this global data store avoid overriding pundit internals.
You might want to read the API docs about CurrentAttributes.
来源:https://stackoverflow.com/questions/56559946/pundit-how-to-handle-multiple-error-codes-for-one-unauthorized-action