✂️

GraphQL-Rubyで認可の責務を Policy 層に委譲する

に公開

TL;DR

  • GraphQL-Rubyでは認可ロジックをTypeに埋め込まずPolicy層に委譲することで以下の恩恵がある。
    • 認可ロジックの保守性/再利用性が高まる
    • 単体テストが書きやすくなる
  • GraphQLはデータ取得とPolicyへの問い合わせだけを責務とし、認可は各GraphQL Typeに対応したPolicyに任せる。

課題

  • 認可条件がResolverやTypeに散在すると要件追加・修正時に複数箇所を修正することになりやすくバグを生む。
  • GraphQL Typeにロジックが埋め込まれると単体テストを書くことが困難。

解決策:Policy層への委譲

  • 認可ロジックをPunditなどのPolicy層に集約させる。
    ※本記事では認可ロジックを集約させるためにPunditというgemを使用します。

https://github.com/varvet/pundit

  • GraphQL-Rubyに組み込まれている authorized?メソッドで認可ロジックを Type に埋め込むことができる。
  • GraphQL 側では Punditの authorize メソッドを呼び出すだけにする。

実装例

Typeに認可ロジックを直書きする例(本記事ではアンチパターンとします)

GraphQL Type

class Types::UserType < Types::BaseObject
  field :id, ID, null: false
  field :email, String, null: false

  def self.authorized?(object, context)
    user = context[:current_user]
    # 認可条件を直書き
    if user&.admin? || user&.id == object.id
      true
    else
      raise GraphQL::ExecutionError, "Not permitted"
    end
  end
end

Policyに委譲した例(グッドパターン)

GraphQL Type

class Types::UserType < Types::BaseObject
  def self.authorized?(object, context)
    Pundit.authorize(context[:current_user], object, :show?)
    true
  rescue Pundit::NotAuthorizedError
    raise GraphQL::ExecutionError, "You do not have permission"
  end
end

Policy

class UserPolicy < ApplicationPolicy
  def show?
    user.admin? || user.id == record.id
  end
end
  • UserPolicyshow? を実装
  • context[:current_user] を使いまわす

実装ポイント

  • GraphQL の contextcurrent_user を仕込む。
  • Policy 層をアクション単位(show?, update? 等)で細かく分ける。
  • エラー発生時は GraphQL::ExecutionError に変換して一元管理できるようにする。

まとめ

  • 認可ロジックの見通しが良くなる
    • 認可条件が変わっても Policy だけ修正すればよく、GraphQL 側を触る必要がない。
  • 再利用性が高く、他サービスと統一しやすい
    • REST APIでも同じ Policy を使える。
  • テストの分離が容易
    • Policy を単体テストできるので認可ルールを GraphQL テストに頼る必要がない。

注意点・ベストプラクティス

  • Policy側で複雑な条件を持たせすぎない。
  • GraphQL では authorize の結果を信頼し、過剰な if を書かない。

Discussion