✂️
GraphQL-Rubyで認可の責務を Policy 層に委譲する
TL;DR
- GraphQL-Rubyでは認可ロジックをTypeに埋め込まずPolicy層に委譲することで以下の恩恵がある。
- 認可ロジックの保守性/再利用性が高まる
- 単体テストが書きやすくなる
- GraphQLはデータ取得とPolicyへの問い合わせだけを責務とし、認可は各GraphQL Typeに対応したPolicyに任せる。
課題
- 認可条件がResolverやTypeに散在すると要件追加・修正時に複数箇所を修正することになりやすくバグを生む。
- GraphQL Typeにロジックが埋め込まれると単体テストを書くことが困難。
解決策:Policy層への委譲
- 認可ロジックをPunditなどのPolicy層に集約させる。
※本記事では認可ロジックを集約させるためにPunditというgemを使用します。
- 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
-
UserPolicyにshow?を実装 - context[:current_user] を使いまわす
実装ポイント
- GraphQL の
contextにcurrent_userを仕込む。 - Policy 層をアクション単位(
show?,update?等)で細かく分ける。 - エラー発生時は
GraphQL::ExecutionErrorに変換して一元管理できるようにする。
まとめ
-
認可ロジックの見通しが良くなる
- 認可条件が変わっても Policy だけ修正すればよく、GraphQL 側を触る必要がない。
-
再利用性が高く、他サービスと統一しやすい
- REST APIでも同じ Policy を使える。
-
テストの分離が容易
- Policy を単体テストできるので認可ルールを GraphQL テストに頼る必要がない。
注意点・ベストプラクティス
- Policy側で複雑な条件を持たせすぎない。
- GraphQL では
authorizeの結果を信頼し、過剰な if を書かない。
Discussion