1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

graphql-rubyでvisibilityを使ってみる

Posted at

visibilityとは

直訳したまんまの意味でGraphQLでもtypeやfieldの可視性を意味するみたいです。

許可していないクライアントには特定のtypeやfiledを見せないようにできます。

もちろんtypeやfiledを見ることすらできないため、typeやfieldを指定してクエリを投げても値は帰ってきません。

ユースケース

  • 開発中のスキーマ情報を外部に公開したくないとき
  • 特定のユーザにしかスキーマ情報を知られたくないとき、使わせたくないとき

Gitlabでも使われているみたいですね

https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#feature-flags

認証との違い

認証と違うのは許可されていないクライアントにtypeやfieldの存在すら知らせないことです。

visibilityを利用すると許可されていないクライアントがIntrospectionを実行しても特定のtypeやfiledを返さないようにできます。

どのような挙動か

簡単なobject typeを例に取ってみます。

module Types
  class QueryType < Types::BaseObject
    field :first_user, Types::UserType, null: false

    def first_user
      User.first
    end
  end
end

class Types::UserType < Types::BaseObject
  field :name, String, null: false

  def name
    object.name
  end
end

次に上の型をprint_schemaを使ってSDLに変換します。

自分はrailsで実装しているのでrails consoleを使っています。

また、schemaクラスの記述は割愛します。

 rails c
> printer = GraphQL::Schema::Printer.new(GraphqlTestSchema)
> puts printer.print_schema

type Query {
  firstUser: User!
}

type User {
  name: String!
}

通常このような形でスキーマ定義を見ることができると思います。

ですが、graphql-rubyのvisibilityという機能を使って、object typeをごっそり見えなくしたいと思います。

class Types::UserType < Types::BaseObject
  field :name, String, null: false

	# 追加したメソッド
	def self.visible?(context)
    false
  end

  def name
    object.name
  end
end

もう一度print_schemaを実行します。

 rails c
> printer = GraphQL::Schema::Printer.new(GraphqlTestSchema)
> puts printer.print_schema

type Query {
}

すると、type Userが消え、それを型に持つquery firstUserごと見えなくなりました。

また、以下のようにqueryを叩いてもfirstUserというquery typeごと存在しないとエラーが帰ってきます。

 rails c
> query = "{
	  firstUser {
      name
    }
  }"
> GraphqlTestSchema.execute(query).to_h
{"errors"=>                                                                  
  [{"message"=>"Field 'firstUser' doesn't exist on type 'Query'",            
    "locations"=>[{"line"=>2, "column"=>5}],                                 
    "path"=>["query", "firstUser"],                                          
    "extensions"=>{"code"=>"undefinedField", "typeName"=>"Query", "fieldName"=>"firstUser"}}]}

実装方針

こちらのページを見ていくと色んなやり方があるのが分かりました。

https://graphql-ruby.org/schema/dynamic_types.html

visible?を使う

visible? はcontextを受け取るため、以下のようにuserに紐付いた属性で可視有無を判定できます。

class Types::UserType < Types::BaseObject
  field :name, String, null: false

	def self.visible?(context)
	  context[:current_user]&.staff?
	end

	def name
    object.name
  end
end

fieldの可視性をカスタマイズしたい場合は以下のようにやります。

まず、BaseField classにインスタンスメソッドとしてvisible? を作成します。

module Types
  class BaseField < GraphQL::Schema::Field
    attr_reader :visible_user

    def initialize(*args, visible_user: '', **kwargs, &block)
      super(*args, **kwargs, &block)
      @visible_user = visible_user
    end
  
    def visible?(context)
      super && case visible_user
      when 'staff'
        context[:current_user]&.staff?
      when 'admin'
        context[:current_user]&.admin?
      else
        true
      end
    end
  end
end

optionとしてfieldにvisible_userを追加することで、staff属性のuserのみが閲覧できるfieldにすることができます。

class Types::UserType < Types::BaseObject
	# option: visible_userを追加
  field :name, String, null: false, visible_user: 'staff'

	def name
    object.name
  end
end

Directiveを使う

GraphQL::Schema::Directive::Flagged を使うことでSDLに変換したときに@flagged を付与することができます。

(参考: https://graphql-ruby.org/type_definitions/directives)

まずはtypeの可視性をカスタマイズする方法から。

class Types::UserType < Types::BaseObject
  field :name, String, null: false

	# directiveの指定
	directive GraphQL::Schema::Directive::Flagged, by: 'staff'

	def name
    object.name
  end
end

このdirectiveを使用する際はcontextにflags配列を追加する必要があります。

ここにbyで指定した値が有ると閲覧可能で無いと閲覧不可という仕様になっています。

class GraphqlController < ApplicationController
  def execute
    variables = prepare_variables(params[:variables])
    query = params[:query]
    operation_name = params[:operationName]
    context = {
      current_user: current_user,
      flags: current_user&.roles #=>['staff']
    }
    result = GraphqlTestSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
(以下中略)

SDLにしてみましょう。

 rails c
> printer = GraphQL::Schema::Printer.new(GraphqlTestSchema, context: {flags: ['staff']})
> puts printer.print_schema
"""
Hides this part of the schema unless the named flag is present in context[:flags]
"""                                                                      
directive @flagged(                                                      
  """                                                                    
  Flags to check for this schema member                                  
  """                                                                    
  by: [String!]!                                                         
) on ARGUMENT_DEFINITION | ENUM | ENUM_VALUE | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | INPUT_OBJECT | INTERFACE | OBJECT | SCALAR | UNION
                                                                         
(中略)

type User @flagged(by: ["staff"]) {
	name: String!
}

このように@flaggedがtype Userに追加されているので、クライアントも分かりやすいですね!

fieldの可視性をカスタマイズする方法もほとんど同じです。

class Types::UserType < Types::BaseObject
  field :name, String, null: false, directives: { GraphQL::Schema::Directive::Flagged => { by: 'staff'} }

	def name
    object.name
  end
end

directivesをfieldに指定するだけで他はtypeと同じですね。

 rails c
> printer = GraphQL::Schema::Printer.new(GraphqlTestSchema, context: {flags: ['staff']})
> puts printer.print_schema
"""
Hides this part of the schema unless the named flag is present in context[:flags]
"""                                                                      
directive @flagged(                                                      
  """                                                                    
  Flags to check for this schema member                                  
  """                                                                    
  by: [String!]!                                                         
) on ARGUMENT_DEFINITION | ENUM | ENUM_VALUE | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | INPUT_OBJECT | INTERFACE | OBJECT | SCALAR | UNION
                                                                         
(中略)

type User {
	name: String! @flagged(by: ["staff"])
}

今度はnameというfieldに@flaggedが付与されています。

まとめ

今回はgraphql-rubyでvisibleを扱うための様々な方法を書いてみました。

個人的にはdirectiveを使う方法がtypeとfieldで統一されていて、更に指定する方法も簡単なのでよさそうに思えました!

contextにあるuser情報から柔軟にvisiblityをカスタマイズできますし、セキュリティ面においての活躍が期待できる機能だと思いますので、使ったことがない方は是非お試しください。

参考資料

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?