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