graphql-rubyでvisibilityを使ってみる

# visibilityとは

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

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

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

### ユースケース

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

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

[https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#feature-flags](https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#feature-flags)

### 認証との違い

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

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

# どのような挙動か

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

```ruby
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クラスの記述は割愛します。

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

type Query {
  firstUser: User!
}

type User {
  name: String!
}
```

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

ですが、graphql-rubyの[visibility](https://graphql-ruby.org/authorization/visibility.html#object-visibility)という機能を使って、object typeをごっそり見えなくしたいと思います。

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

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

  def name
    object.name
  end
end
```

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

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

type Query {
}
```

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

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

```ruby
❯ 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](https://graphql-ruby.org/schema/dynamic_types.html)

## visible?を使う

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

```ruby
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?` を作成します。

```ruby
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にすることができます。

```ruby
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](https://graphql-ruby.org/api-doc/2.0.2/GraphQL/Schema/Directive/Flagged) を使うことでSDLに変換したときに`@flagged` を付与することができます。

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

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

```ruby
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で指定した値が有ると閲覧可能で無いと閲覧不可という仕様になっています。

```ruby
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にしてみましょう。

```ruby
❯ 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の可視性をカスタマイズする方法もほとんど同じです。

```ruby
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と同じですね。

```ruby
❯ 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をカスタマイズできますし、セキュリティ面においての活躍が期待できる機能だと思いますので、使ったことがない方は是非お試しください。

# 参考資料

- [https://graphql-ruby.org/schema/dynamic_types.html](https://graphql-ruby.org/schema/dynamic_types.html)
- [https://graphql-ruby.org/type_definitions/directives.html#schema-directives](https://graphql-ruby.org/type_definitions/directives.html#schema-directives)
- [https://graphql-ruby.org/authorization/visibility.html](https://graphql-ruby.org/authorization/visibility.html)
- [https://graphql-ruby.org/api-doc/2.0.2/GraphQL/Schema/Directive/Flagged.html](https://graphql-ruby.org/api-doc/2.0.2/GraphQL/Schema/Directive/Flagged.html)