GraphQL × CanCanCan: fieldレベル認可の実装
2022年に取り組んだ内容です。サンプルコードに変更したり、当時のことを思い出して書いているので、抜けや漏れ等があるかもしれません。また、分かりづらい部分もあると思います。
複雑な権限要件を持つアプリケーションでは、データへのアクセスを細かく制御することが不可欠です。特にGraphQLを採用している場合、クライアント主導のデータ取得という柔軟性がもたらす新たな認可の課題に直面します。本記事では、Ruby on RailsアプリケーションでCanCanCanとGraphQLを組み合わせたfieldレベルの認可実装について、実践的なアプローチを解説します。
目次
GraphQLにおける認可の課題
RESTful APIと異なり、GraphQLではクライアントが必要なデータを正確に指定できる柔軟性が大きな特徴です。しかし、この柔軟性は認可の観点から新たな課題をもたらします:
主な課題
- 階層構造による権限の連鎖 - GraphQLのネストされたリゾルバでは、親オブジェクトにアクセスできれば、その子孫fieldにも暗黙的にアクセスできてしまいます。
- field単位での制御の必要性 - ビジネス要件として、同一オブジェクト内の特定fieldのみ特定ロールに制限したいケースが頻出します。
-
認証と認可の明確な区別 - システム全体で一貫したアクセス制御ポリシーを維持するために、認証と認可を適切に分離する必要があります。
- 当時の課題としては、新しい機能が認証情報がないユーザーへ情報提供だったので、セキュリティの課題があった。
例えば、商品情報(Product
)のデータ構造において、管理者にはコスト情報を表示する一方、一般ユーザーには見せたくないといった要件があります:
query {
product(id: "123") {
name
description
# 管理者にのみ表示したいfield
supplierCost
profitMargin {
percentage
value
}
}
}
これはGraphQLの階層的なデータモデルでは特に難しい問題で、単純にモデルレベルの認可だけでは解決できません。
CanCanCanによるロールベース認可の構成
CanCanCanの基本思想
CanCanCanは、Railsアプリケーションにおいて最も広く使われている認可ライブラリの一つです。その基本思想はリソースベースの認可にあります。ユーザーが「何ができるか」ではなく、「どのリソースに対して何ができるか」という視点でアクセス制御を行います。
この思想は以下の形式で表現されます:
can :action, Subject
ここで:
-
:action
は:read
,:create
,:update
,:destroy
などの操作を表します -
Subject
はモデルクラスや特定のインスタンスを表します
例えば、「ユーザーは投稿を読むことができる」という権限は次のように定義します:
can :read, Post
さらに条件を追加することもできます:
can :update, Post, author_id: user.id
これは「ユーザーは自分が作成した投稿のみ更新できる」ことを意味します。このリソースベースの考え方は、RESTful APIと非常に相性が良く、モデル中心の認可ロジックを構築できます。
GraphQLに対応する場合は、この思想を拡張してfieldレベルの認可に適用します。つまり「どのリソースのどのfieldに対して何ができるか」という考え方になります。
GraphQL-Proとの統合
GraphQL Ruby Proを使用する場合、CanCanCanとの統合機能が公式にサポートされています。これにより、既存のCanCanCanの認可ロジックをGraphQLに簡単に適用できます。
ただし、本記事で説明するフィールドレベルの細かい制御は、追加の拡張が必要になります。
GraphQL-Pro がなくても、この記事で説明する方法でCanCanCanとGraphQLを統合できますが、Pro版を使用すると基本的な統合がより簡単になります。
まず、複数のユーザーロールに対応するために、CanCanCanの認可ロジックを整理する方法を見ていきましょう。
モジュールによる能力定義の分割
認可ロジックを管理しやすくするために、ロールごとに別々のモジュールに分割することが効果的です:
# app/models/ability.rb
class Ability
include CanCan::Ability
include Abilities::AdminAbility
include Abilities::ManagerAbility
include Abilities::StaffAbility
include Abilities::RegularUserAbility
include Abilities::GuestAbility
attr_reader :user, :session
def initialize(user, session)
@user = user
@session = session
unless @user
guest_ability
return
end
case user.role
when 'admin'
admin_ability
when 'manager'
manager_ability
when 'staff'
staff_ability
when 'regular'
regular_user_ability
else
guest_ability
end
end
end
このように分割することで、各ロールの認可ロジックをそれぞれ独立して管理でき、コードの可読性と保守性が向上します。特に注目すべきは、未認証ユーザーにもguest_ability
として明示的な権限セットを定義している点です。
各Abilityモジュールの実装例
それぞれのロールに対する認可ロジックは、専用のモジュールに定義します:
# app/models/abilities/staff_ability.rb
module Abilities
module StaffAbility
def staff_ability
# モデルレベルでの基本認可
can :read, Product
# その他のリソースに対する権限...
end
end
end
# app/models/abilities/admin_ability.rb
module Abilities
module AdminAbility
def admin_ability
can :read, Product
# 管理者のみアクセス可能
can :read, ::ProductType::AdminOnly # サンプルコードなので命名は適当です。
# その他のリソースに対する権限...
end
end
end
この方法により、ロールごとにフィールドレベルの権限を詳細に定義できます。can :read, CustomName
の形式でフィールド固有の権限を指定しています。
CanCanCanを採用した理由
私たちのプロジェクトにおいて、GraphQL-Rubyが提供するField Visibility
機能も検討しました。この機能は以下のように実装できます:
# GraphQL-Rubyの公式ドキュメントに基づく実装例
class Types::BaseField < GraphQL::Schema::Field
def visible?(context)
super && context[:current_user].can_view?(self.name)
end
end
しかし、以下の理由から最終的にCanCanCanを使った実装アプローチを選択しました:
-
既存の認可システムとの統一性: プロジェクト全体で既にCanCanCanを使用しており、RESTfulエンドポイントやコントローラーレベルでの認可ロジックが整備されていました。GraphQLでも同じ認可システムを使用することで、アプリケーション全体で一貫した権限管理が可能になります。
-
役割とポリシーの再利用: ユーザーロールやポリシーの定義が既にCanCanCanで実装されていたため、これらを再利用することで開発効率と保守性が向上しました。
-
チームの習熟度: 開発チームはCanCanCanの使用経験が豊富であり、学習コストを最小限に抑えつつ、高度な認可システムを構築できました。
-
統合された監視とエラー報告: 既存のCanCanCanエラーハンドリングとSentryによる監視体制を活かし、認可問題の早期発見と対応が可能になりました。
Field Visibilityを使った方法も十分に機能しますが、私たちの場合はアプリケーション全体での認可の一貫性とコード再利用の観点から、CanCanCanを拡張する選択をしました。この決断により、GraphQLとRESTful APIの両方で同じ認可ロジックが適用され、メンテナンス性と一貫性が向上しました。
また、今回実装したCanCanCanとGraphQLの統合アプローチにより、将来的な要件変更にも柔軟に対応できる基盤が整いました。例えば、新しいユーザーロールやフィールドレベルのアクセス制御が必要になった場合でも、単一の場所(Abilityモジュール)で定義を追加するだけで済みます。
fieldレベル認可の実装
GraphQLスキーマにCanCanCanの認可チェックを統合するための核心部分を見ていきましょう。
BaseFieldクラスの拡張
認可ロジックをGraphQLのフィールド解決プロセスに組み込むには、GraphQL Ruby の BaseField
クラスを拡張します:
# app/graphql/types/base_field.rb
module Types
class BaseField < GraphQL::Schema::Field
argument_class Types::BaseArgument
# queryの全fieldに認可を通します
#
# @param subject [Class] CanCanCanの認可用に設定する
# fieldの戻り値がHash(modelなし)やActiveRecord::Relation等の場合に設定
# ex)
# app/graphql/types/statistics_type.rb
# field :value, String, null: false, subject: self
def initialize(*args, subject: nil, load_association: false, **kwargs, &block)
@subject = subject
if load_association
super(*args, resolver_method: :load_association, extras: [:graphql_name], **kwargs, &block)
else
super(*args, **kwargs, &block)
end
end
def authorized?(obj, args, ctx)
return true unless obj
subject = @subject || obj.class # subjectの引数があればそれを優先的に認可にかけます。
super && ctx[:current_ability].can?(:read, subject, ctx[:current_field].original_name)
end
end
end
この実装の重要なポイント:
-
subject
パラメータ - フィールドごとに認可チェックの対象を明示的に指定できるようにしています。これは特に非ActiveRecordオブジェクトや合成オブジェクトを返すフィールドで役立ちます。 -
フィールド名を活用した認可チェック -
ctx[:current_field].original_name
を使って、現在処理中のフィールド名をCanCanCanに渡し、そのフィールドに対する権限をチェックします。 - キャッシュの使用 - パフォーマンスを向上させるために、同一リクエスト内で同じフィールドに対する権限チェックをキャッシュします。
コンテキスト管理の実装
フィールドレベルの認可には、現在のフィールド情報などのコンテキスト情報が必要です。これを追加するモジュールを実装します:
# app/graphql/additional_context.rb
module AdditionalContext
extend ActiveSupport::Concern
private
def current_user
context.fetch(:current_user)
end
def current_ability
context.fetch(:current_ability)
end
...他の実装
# 下記メソッドは任意の箇所で認可を実行する場合に使います。
# Resolver,Mutationの中の認可として使われることが多いです
# ↓のように使います
# authorize!(:read, Product)
# authorize!(:update, product)
# @param action [Symbol]
# @param subject [Class] basically ApplicationRecord's child class
def authorize!(action, subject)
return true if current_ability.can?(action, subject)
class_name = if subject.is_a?(Class)
subject.name
else
subject.class.name
end
access_denied_error = ::CanCan::AccessDenied.new("#{current_user.role}に対して#{class_name}の#{action}の操作は許可されていません", action, subject)
Sentry.capture_exception(access_denied_error)
raise access_denied_error
end
end
authorize!(action, subject)
のエラー通知は下記のようになります。
BaseObjectクラスとのインテグレーション
次に、この拡張したフィールドクラスをGraphQLのオブジェクト型で使用するよう設定します:
# app/graphql/types/base_object.rb
module Types
class BaseObject < GraphQL::Schema::Object
include ::AdditionalContext
field_class Types::BaseField
...
end
end
実際のタイプ定義での使用例
上記の基盤を使って、実際のGraphQLタイプ定義ではこのように認可が考慮されたフィールドを定義できます:
module Types
class ProductType < Types::BaseObject
field :id, ID, null: false
field :name, String, null: false
# コスト関連フィールド - 管理者のみアクセス可能(カスタム認可対象を持つフィールド)
field :supplier_cost, Float, null: true, subject: ::ProductType::AdminOnly # サンプルコードなので命名は適当です。
end
end
このように、各フィールドは自動的に認可チェックを受け、ユーザーが権限を持たないフィールドにはアクセスできなくなります。
※ Mutationのfieldは除外するような実装もしてますが、ここでは省略。
エラーハンドリングとユーザー体験
フィールドレベルの認可チェックが失敗した場合、適切なエラーを返すことが重要です。
未認可フィールドの処理
GraphQL Rubyでは、unauthorized_field
メソッドをオーバーライドすることで、未認可フィールドへのアクセスに対する動作をカスタマイズできます:
# app/graphql/your_app_schema.rb
def self.unauthorized_field(error)
role = error.context[:current_user].role
class_name = error.context[:current_object].object.class
access_denied_error = CanCan::AccessDenied.new("#{role}に対して#{class_name}の操作は許可されていません", nil, error.context[:current_field])
Sentry.capture_exception(access_denied_error)
raise access_denied_error
end
この実装では、GraphQLのエラー形式に沿った詳細情報をクライアントに提供しています。これにより、フロントエンドで適切なエラーハンドリングが可能になります。ただし、本番環境ではセキュリティ上の理由からエラーメッセージを簡略化することも検討すべきでしょう。
全fieldに対して認可を設定しているので、下記のように認可漏れや不正なアクセスがないか監視できるようにしています。
まとめと実践的なヒント
GraphQLとCanCanCanを組み合わせたフィールドレベルの認可実装は、複雑な権限要件を持つアプリケーションに大きな柔軟性をもたらします。以下に、実践で役立つヒントをまとめます:
ベストプラクティス
- 認証と認可を明確に分離する - 認証状態も認可ルールとして表現することで、アクセス制御ロジックを一元管理できます。
- モジュール化と再利用 - ロール別にAbilityモジュールを分割し、共通パターンを抽出することで、メンテナンス性を向上させます。
- 明示的な権限定義 - デフォルトは「拒否」として、必要な権限を明示的に許可する方針を取ります。
- フィールド名の統一 - GraphQLのフィールド名とモデルの属性名を一致させると、権限定義が直感的になります。
- 問題発生時のデバッグを容易にする - フィールドレベルの認可に関する問題が発生した場合、適切なログ記録やエラーメッセージが役立ちます。
注意点
- 権限爆発の回避 - 過度に細かな権限定義はメンテナンスの負担になります。ビジネス要件に基づいて適切な粒度を選びましょう。
- クライアント体験の考慮 - 認可エラーはクライアントにとって理解しやすい形で伝える必要があります。エラー表示のUXにも配慮しましょう。
- セキュリティ意識 - エラーメッセージには内部情報を含めないよう注意し、認可システムの詳細が外部に漏れないようにします。
フィールドレベルの認可は、GraphQLの柔軟性を活かしながら適切なセキュリティを確保するための重要な要素です。この記事で紹介したパターンを出発点として、あなたのプロジェクトに最適なアプローチを見つけてください。
この記事を通して、GraphQLにおけるフィールドレベルの認可実装について、基本的な概念から実践的なパターン、そして進化の過程までを解説しました。GraphQL-Proを使用すればCanCanCanとの基本的な統合はさらに簡単になりますが、本記事で紹介した手法を応用することで、より細かなフィールドレベルの制御が可能になります。これらの知識が、あなたのGraphQL APIにおける堅牢なアクセス制御実装の一助となれば幸いです。
Discussion