GraphQLでの権限制御について

これは[Supershipグループ Advent Calendar 2020](https://qiita.com/advent-calendar/2020/supership)の14日目の記事です。

## はじめに

GraphQLでAPIの実装で権限を付けるのに検討した内容です。
検証コードは[graphql-ruby](https://github.com/rmosolgo/graphql-ruby)を使ってます。

```ruby:Gemfile
source 'https://rubygems.org'

gem 'graphql', '~> 1.11'
```

## 権限エラーをどう扱うかについて

作ってるAPIによってどう扱うのが適切か検討する必要はあります。

#### エラーメッセージのレスポンスを返すケース

エラー用のレスポンスを返すケースです。

```json
{
  "errors" : [
    {
      "message" : "Not authorized"
    }
  ]
}
```

#### ユーザーに権限のないことを気づかれないように返すケース

ユーザーから権限があるかを知られたくないケースになります。
懸念としてはそのデーターがなくて空のなのか権限がなくて空にしてるのかがデバッグする時に分かりづらいという点があります。

##### 配列以外の型のケース

```ruby
class UserType < GraphQL::Schema::Object
  field :name, ID, null: true

  def name
    context[:admin] ? object.name : nil
  end
end
```

##### 配列型のケース

```ruby
class PostType < GraphQL::Schema::Object
  field :id, ID, null: false
end

class UserType < GraphQL::Schema::Object
  field :posts, [PostType], null: false

  def posts
    context[:admin] ? object.posts : []
  end
end
```

## 権限の設定が漏れやすいケース

* postの情報は全てのuserから取得できる
* userの情報はadminしか取得できない

という仕様にした場合

```ruby
require 'graphql'

USERS = {
  '1' => {
    id: '1',
    name: 'user1'
  }
}

POSTS = {
  '1' => {
    id: '1',
    user: USERS['1']
  }
}

class UserType < GraphQL::Schema::Object
  field :id, ID, null: false
  field :name, ID, null: false
end

class PostType < GraphQL::Schema::Object
  field :id, ID, null: false
  field :user, UserType, null: false
end

class QueryType < GraphQL::Schema::Object
  field :user, UserType, null: false do
    argument :id, ID, required: true
  end

  field :post, PostType, null: false do
    argument :id, ID, required: true
  end

  def user(id:)
    return USERS[id] if context[:admin]

    raise GraphQL::ExecutionError, 'Not authorized'
  end

  def post(id:)
    POSTS[id]
  end
end

class Schema < GraphQL::Schema
  query(QueryType)
end

query = <<-GRAPHQL
query($id: ID!) {
  user(id: $id) {
    id
    name
  }
}
GRAPHQL

puts Schema.execute(query, context: { admin: false }, variables: { id: 1 }).to_h
```

```graphql
query($id: ID!) {
  user(id: $id) {
    id
    name
  }
}
```

このクエリを実行した場合はuserのid、nameが取得できず権限エラーになりますが

```graphql
query($id: ID!) {
  post(id: $id) {
    user {
      id
      name
    }
  }
}
```

このクエリを実行した場合は権限エラーにならずuserのid, nameを取得できてしまいます。
userのid, nameをpost経由で取りに行くことでadminじゃなくてもuserのid, nameが取得できてしまうという権限漏れが起きてしまいます。

### 修正したコード

```ruby
require 'graphql'

USERS = {
  '1' => {
    id: '1',
    name: 'user1'
  }
}

POSTS = {
  '1' => {
    id: '1',
    user: USERS['1']
  }
}

class UserType < GraphQL::Schema::Object
  field :id, ID, null: false
  field :name, ID, null: false

  def id
    return USERS[id] if context[:admin]

    raise GraphQL::ExecutionError, 'Not authorized'
  end

  def name
    return USERS[id] if context[:admin]

    raise GraphQL::ExecutionError, 'Not authorized'
  end
end

class PostType < GraphQL::Schema::Object
  field :id, ID, null: false
  field :user, UserType, null: false
end

class QueryType < GraphQL::Schema::Object
  field :user, UserType, null: false do
    argument :id, ID, required: true
  end

  field :post, PostType, null: false do
    argument :id, ID, required: true
  end

  def user(id:)
    USERS[id]
  end

  def post(id:)
    POSTS[id]
  end
end

class Schema < GraphQL::Schema
  query(QueryType)
end

query = <<-GRAPHQL
query($id: ID!) {
  post(id: $id) {
    user {
      id
      name
    }
  }
}
GRAPHQL

puts Schema.execute(query, context: { admin: false }, variables: { id: 1 }).to_h
```

このように漏れなく権限を考慮するとフィールド毎に権限チェックをする必要が出ます。

### 対策

* フィールドレベルで権限管理する
  フィールドレベルで権限管理した時に全部のフィールドに対して1つずつ権限を定義していくのは大変なので[grapqh-gurad](https://github.com/exAspArk/graphql-guard)を使うと記述量は減らせます。

* リクエスト時のクエリのネストとフィールドの上限を設定する
  根本的な対策ではないですが、クエリのネストする深さとリクエストするフィールド数に上限を設定することで、万が一権限の実装が誤ってた場合の被害を最小限にできます。

```ruby
class Schema < GraphQL::Schema
  max_complexity 80
  max_depth 8
end
```

## 最後に

* GraphQLで定義した型に対して、このフィールドはアクセスできるがこのフィールドはアクセスできないという権限制御を漏れなく行おうとするとフィールド単位で権限を付ける必要が出ます

* 権限を実装する場合は関連経由だと取得できてしてしまわないかを気をつける必要があります

***

Supershipではプロダクト開発やサービス開発に関わる人を絶賛募集しております。
ご興味がある方は以下リンクよりご確認ください。
[Supership株式会社 採用サイト](https://supership.jp/recruit/)
是非ともよろしくお願いします。