GraphQL Rubyのvisible?, authorized?について

GraphQLでは、MutationやQueryのそれぞれのobject, field単位などで、認可を行います。
これにより、どのようなクエリが発行されても、ユーザーが参照する権限を持っているかを各field, objectなどの単位で確認して、結果を返すことができます。

contextでユーザーの情報を持ち、認可を行います。

## ビジネスロジックレイヤーでの認可設定

この記事では、GraphQL RubyのVisibility、Authorizationについて説明しますが、そのようなGraphQLレイヤーでの認可の前に、ビジネスロジックレイヤーでの認可を検討すべきと言われています。

https://graphql.org/learn/thinking-in-graphs/#business-logic-layer

例えば、Modelにロジックを書くことで、すべてのfieldに同じ処理を書く必要がなくなります。

https://graphql-ruby.org/authorization/overview.html#authorization-in-your-business-logic

```ruby
class Post < ActiveRecord::Base
  def self.posts_for(user)
    if user.admin?
      self.all
    else
      self.published
    end
  end
end
```

```ruby
field :posts, [Types::Post], null: false

def posts
  Post.posts_for(context[:current_user])
end
```

一方で、GraphQLのレイヤーで認可を行うのにもメリットがあります。
APIレイヤーが安全であることをさらに保証できたり、実行する前に認可ができることです。

GraphQL Rubyには、VisibilityとAuthorizationの機能があるので[^1]こちらについて説明していきます。

[^1]: GraphQL::ProにはCanCanとPunditのインテグレーションがあるようです。

## Visibility と Authorization

https://graphql-ruby.org/authorization/visibility.html

https://graphql-ruby.org/authorization/authorization.html

どちらも、あるfieldやobject(型クラス)などに対してユーザーの権限に応じて参照できる情報の制限を設定できます。
それぞれ以下のような特徴があります。

- Visibility: `visible?`
    - スキーマの一部を隠すことができる
    - 権限のないユーザーが参照しようとするとバリデーションエラーを返す
- Authorization: `authorized?`
    - 権限のないユーザーも、fieldやobjectをIntrospectionで見ることができる
    - 権限のないユーザーが参照しようとすると`nil`を返す

`visible?`はGraphQL仕様ではなく、GraphQL Rubyの機能としてあるものです。
一般ユーザーには存在を知られたくない開発中の機能を、特定のメンバーにのみ公開するときなどに使うことが想定されています。

どちらもobject、field、引数などに`visible?`, `authorized?`メソッドを定義し、それぞれ設定することができます。

### field

field単位で、隠す・認可の設定を検証してみます。
BaseFieldで`visible?`、`authorized?`を定義します。
それぞれのメソッドが、falseを返すと、ユーザーは参照できなくなります。

```ruby
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ユーザーのみが参照できるようになります。

```diff_ruby
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ユーザーの場合は以下のように参照することができますが、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/b9203b09-1697-0807-57b0-abc9c50b0795.png)

adminではない場合、このように`name`は`visible?`の設定によってfieldが参照できず、エラーが返されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/68d0dda2-c355-2274-61cd-9ddc94546b7a.png)

fieldを見ても、nameが隠されていることが分かります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/f4455a1c-3b62-7618-6b34-f4893eefad83.png)

`authorized?`が設定されているemail fieldはエラーにはなりませんが、nullが返ります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/4ddabe27-499f-48ad-3166-1833757036f6.png)

### Object

次はObjectに設定してみます。

```diff_ruby
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も隠されていることが分かります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/0467d135-ea8f-c229-c039-da4cb6557b26.png)

authorized?の場合は、エラーにはならずに、nullが返ります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/38fba8b2-4d09-7ca3-d762-f72775520191.png)


## authorized? のエラーのカスタマイズ

`authorized?`を設定したobject, fieldを参照したときには、特にエラーが発生せずにnullが返されますが、これをカスタマイズすることもできます。

https://graphql-ruby.org/authorization/authorization.html#handling-unauthorized-objects

このようにSchemaに設定することで、

```ruby
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
```

このようにエラーの情報を追加することもできるようです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/b359db23-13c8-6e27-9d6a-ca3c449cb3a8.png)

## 参考

- https://graphql.org/learn/thinking-in-graphs/#business-logic-layer
- https://graphql-ruby.org/authorization/overview.html