Pundit:如何处理一个未经授权的操作的多个错误代码?

Pundit: how to handle multiple error codes for one unauthorized action?

我使用专家来处理我的 API 政策,我有一个项目展示可以在某些情况下禁止用户使用,而在其他情况下只是限制。受限是指现在禁止使用,但如果他付费,他就可以访问它。所以我需要我的 API 以特定代码 (402 Payment Required) 响应,以便客户可以邀请用户付款以解锁节目。

这是我当前的代码,它仅在专家 return 为假时以 403 响应。

最好在什么地方实施 return 403402 的条件,以便干燥和清洁?

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

不幸的是,Pundit 无法开箱即用地处理不同的错误类型。它被构建为始终期望策略的方法为 return true 或 false false。因此,在控制器中引发另一个自定义错误并从中拯救将不起作用,因为它也会破坏视图方法。

我建议采用一种解决方法来引入不同的错误类型。这样的事情可能会奏效:

# 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

我不认为使用全局 CurrentAttributes 是一个好习惯,但它们是 Rails 的一部分,在这种情况下,使用这个全局数据存储可以避免覆盖专家内部。

您可能想要阅读有关 CurrentAttributes 的 API 文档。

在app/controllers/concerns/response.rb

中创建响应模块
module Response
  def json_response(object, status = :ok)
    render json: object, status: status
  end
end

在 app/controllers/concerns/exception_handler.rb

中创建异常处理程序
module ExceptionHandler
  extend ActiveSupport::Concern

  included do
    rescue_from Pundit::NotAuthorizedError, with: :unauthorized_request
  end

  private

  # JSON response with message; Status code 401 - Unauthorized
  def unauthorized_request(e)
    json_response({ message: e.message }, :unauthorized)
  end
end

在app/controllers/application_controller.rb

class ApplicationController < ActionController::API
   include Response
   include ExceptionHandler
end

就是这样