GraphQLでは、MutationやQueryのそれぞれのobject, field単位などで、認可を行います。
これにより、どのようなクエリが発行されても、ユーザーが参照する権限を持っているかを各field, objectなどの単位で確認して、結果を返すことができます。
contextでユーザーの情報を持ち、認可を行います。
ビジネスロジックレイヤーでの認可設定
この記事では、GraphQL RubyのVisibility、Authorizationについて説明しますが、そのようなGraphQLレイヤーでの認可の前に、ビジネスロジックレイヤーでの認可を検討すべきと言われています。
例えば、Modelにロジックを書くことで、すべてのfieldに同じ処理を書く必要がなくなります。
class Post < ActiveRecord::Base
def self.posts_for(user)
if user.admin?
self.all
else
self.published
end
end
end
field :posts, [Types::Post], null: false
def posts
Post.posts_for(context[:current_user])
end
一方で、GraphQLのレイヤーで認可を行うのにもメリットがあります。
APIレイヤーが安全であることをさらに保証できたり、実行する前に認可ができることです。
GraphQL Rubyには、VisibilityとAuthorizationの機能があるので1こちらについて説明していきます。
Visibility と Authorization
どちらも、あるfieldやobject(型クラス)などに対してユーザーの権限に応じて参照できる情報の制限を設定できます。
それぞれ以下のような特徴があります。
- Visibility:
visible?
- スキーマの一部を隠すことができる
- 権限のないユーザーが参照しようとするとバリデーションエラーを返す
- Authorization:
authorized?
- 権限のないユーザーも、fieldやobjectをIntrospectionで見ることができる
- 権限のないユーザーが参照しようとすると
nil
を返す
visible?
はGraphQL仕様ではなく、GraphQL Rubyの機能としてあるものです。
一般ユーザーには存在を知られたくない開発中の機能を、特定のメンバーにのみ公開するときなどに使うことが想定されています。
どちらもobject、field、引数などにvisible?
, authorized?
メソッドを定義し、それぞれ設定することができます。
field
field単位で、隠す・認可の設定を検証してみます。
BaseFieldでvisible?
、authorized?
を定義します。
それぞれのメソッドが、falseを返すと、ユーザーは参照できなくなります。
module Types
class BaseField < GraphQL::Schema::Field
argument_class Types::BaseArgument
def initialize(*args, admin_only_visible: false, admin_only_authorized: false, **kwargs, &block)
@admin_only_visible = admin_only_visible
@admin_only_authorized = admin_only_authorized
super(*args, **kwargs, &block)
end
def visible?(ctx)
super && (@admin_only_visible ? ctx[:current_user]&.admin? : true)
end
def authorized?(obj, args, ctx)
super && (@admin_only_authorized ? ctx[:current_user]&.admin? : true)
end
end
end
そして制限したいfieldでそれぞれadmin_only_visible
, admin_only_authorized
を設定すると、adminユーザーのみが参照できるようになります。
module Types
class UserType < Types::BaseObject
field :id, ID, null: false
- field :name, String, null: false
- field :email, String, null: true
+ field :name, String, null: false, admin_only_visible: true
+ field :email, String, null: true, admin_only_authorized: true
end
end
これによって、adminユーザーの場合は以下のように参照することができますが、
adminではない場合、このようにname
はvisible?
の設定によってfieldが参照できず、エラーが返されます。
fieldを見ても、nameが隠されていることが分かります。
authorized?
が設定されているemail fieldはエラーにはなりませんが、nullが返ります。
Object
次はObjectに設定してみます。
module Types
class UserType < Types::BaseObject
field :id, ID, null: false
field :name, String, null: false
field :email, String, null: true
+ field :articles, ArticleType.connection_type, null: false
end
end
module Types
class ArticleType < Types::BaseObject
field :id, ID, null: false
field :title, String, null: false
field :body, String, null: false
field :author, Types::UserType, null: false
+ def self.visible?(context)
+ super && context[:current_user]&.admin?
+ end
+
+ # def self.authorized?(object, context)
+ # super && context[:current_user]&.admin?
+ # end
end
end
visible? の場合は、Userの型としてarticlesが存在しないように見えます。
また、objectだけでなく、その型を返すfieldも隠されていることが分かります。
authorized?の場合は、エラーにはならずに、nullが返ります。
authorized? のエラーのカスタマイズ
authorized?
を設定したobject, fieldを参照したときには、特にエラーが発生せずにnullが返されますが、これをカスタマイズすることもできます。
このようにSchemaに設定することで、
class MySchema < GraphQL::Schema
lazy_resolve(Promise, :sync)
mutation(Types::MutationType)
query(Types::QueryType)
default_max_page_size 20
use GraphQL::Dataloader
def self.unauthorized_object(error)
raise GraphQL::ExecutionError, "An object of type #{error.type.graphql_name} was hidden due to permissions"
end
def self.unauthorized_field(error)
raise GraphQL::ExecutionError, "The field #{error.field.graphql_name} on an object of type #{error.type.graphql_name} was hidden due to permissions"
end
end
このようにエラーの情報を追加することもできるようです。
参考
- https://graphql.org/learn/thinking-in-graphs/#business-logic-layer
- https://graphql-ruby.org/authorization/overview.html
-
GraphQL::ProにはCanCanとPunditのインテグレーションがあるようです。 ↩