GraphQL RubyでIntrospectionクエリをクライアント別に制御する方法

GraphQL Rubyを使用しているプロジェクトで、Introspectionクエリをクライアント毎に有効化、無効化の制御を行いたいことがあったので、その方法を備忘として残しておきたいと思います。

前提

graphql-rubyでは、GraphQL::Schemaの継承クラスでdisable_introspection_entry_pointsを定義することで、Intorospectionのエントリーポイント(__schema,__type)を無効化することがでできます。

公式ドキュメントより

class MySchema < GraphQL::Schema
  disable_introspection_entry_points if Rails.env.production?
end

ただ、例のように本番環境のみIntrospectionクエリを無効化するようなユースケースであれば問題ないですが、リクエスト元のクライアントを判定して、Introspectionクエリの有効、無効を細かく制御したい場合にはそのまま使うことができません。

調べてみたところ、Introspectionを拡張することでContext情報に基づいたエントリーポイントの制御ができるようでした。

また、動作環境は以下を前提とします。

Context情報に基づいたエントリーポイントの制御

GraphQL Rubyの作者のrmosolgoさんのgistでその方法が紹介されているので、参考にしつつ説明していきます。

CustomIntrospectionモジュールを定義しIntrospection名前空間内で、Introspectionクエリで出力される各オブジェクトの定義を上書きます。

  • __Schema: built-in introspection types
  • __schema: introspection entry points
  • __typename: dynamic, globally-available fields

各オブジェクトのFieldやSchema定義のvisible?メソッドをoverrideし、Context情報が特定の条件に該当する場合にのみFieldやSchemaが出力されるようにします。

app/graphql/custom_introspection.rb

module CustomIntrospection
    module HideIntrospectionByContext
      def visible?(ctx)
        super &&
          if introspection?
            ctx[:introspection_permitted]
          else
            true
          end
      end
    end

    # globally-available fields
    class IntrospectionField < GraphQL::Schema::Field
      include HideIntrospectionByContext
    end

    # dynamic fields
    class DynamicFields < GraphQL::Introspection::DynamicFields
      field_class(IntrospectionField)
      field :__typename, String, null: false
    end

    # introspection entry points
    class EntryPoints < GraphQL::Introspection::EntryPoints
      field_class(IntrospectionField)
      field :__type, GraphQL::Introspection::TypeType do
        argument :name, String
      end
    end

    # built-in introspection types
    class SchemaType < GraphQL::Introspection::SchemaType
      extend HideIntrospectionByContext
    end
  end

上記で定義した、作成したカスタムCustomIntrospectionモジュールをGraphQL::Schemaの継承クラスに定義します。

app/graphql/my_schema.rb

class MySchema < GraphQL::Schema

    introspection(CustomIntrospection)

    ...
end

出力の例

通常のクエリは問題なく実行できます(visible?メソッド内のintrospection?でIntrospectionクエリのみを制御の対象にできている)

pp MySchema.execute(things_query_str, context: {}).to_h

# {"data"=>
#   {"things"=>
#     [{"name"=>"Pogo Stick"},
#      {"name"=>"Immersion Blender"},
#      {"name"=>"Ceiling Fan"}]}}

introspection_permittedがtrueの場合はIntrospectionクエリの実行が可能です

pp MySchema.execute(GraphQL::Introspection::INTROSPECTION_QUERY, context: { introspection_permitted: true }).to_h

# {"code"=>"useAndDefineFragment", "fragmentName"=>"FullType"}}]}
# {"data"=>
#   {"__schema"=>
#     {"queryType"=>{"name"=>"Query"},
#      "mutationType"=>nil,
#      "subscriptionType"=>nil,
#      "types"=> # ...

introspection_permittedがContext情報にない場合は、Introspectionクエリに失敗します

pp MySchema.execute(GraphQL::Introspection::INTROSPECTION_QUERY, context: {}).to_h

# {"errors"=>
#   [{"message"=>"Field '__schema' doesn't exist on type 'Query'",
# ...

まとめ

  • GraphQL::Schemaの継承クラスでdisable_introspection_entry_pointsを定義することで、環境変数や定数をベースに全体のIntrospectionクエリの実行可否を制御することは可能
  • クエリを実行してくるクライアント毎にIntrospectionクエリの実行可否を制御するには、Introspection名前空間内で、visible?メソッドのオーバーライドしContextに基づいたスキーマ情報の可視、不可視の切り替えを行うことで実現することができる

参考