これはSupershipグループ Advent Calendar 2020の14日目の記事です。
はじめに
GraphQLでAPIの実装で権限を付けるのに検討した内容です。
検証コードはgraphql-rubyを使ってます。
source 'https://rubygems.org'
gem 'graphql', '~> 1.11'
権限エラーをどう扱うかについて
作ってるAPIによってどう扱うのが適切か検討する必要はあります。
エラーメッセージのレスポンスを返すケース
エラー用のレスポンスを返すケースです。
{
"errors" : [
{
"message" : "Not authorized"
}
]
}
ユーザーに権限のないことを気づかれないように返すケース
ユーザーから権限があるかを知られたくないケースになります。
懸念としてはそのデーターがなくて空のなのか権限がなくて空にしてるのかがデバッグする時に分かりづらいという点があります。
配列以外の型のケース
class UserType < GraphQL::Schema::Object
field :name, ID, null: true
def name
context[:admin] ? object.name : nil
end
end
配列型のケース
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しか取得できない
という仕様にした場合
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
query($id: ID!) {
user(id: $id) {
id
name
}
}
このクエリを実行した場合はuserのid、nameが取得できず権限エラーになりますが
query($id: ID!) {
post(id: $id) {
user {
id
name
}
}
}
このクエリを実行した場合は権限エラーにならずuserのid, nameを取得できてしまいます。
userのid, nameをpost経由で取りに行くことでadminじゃなくてもuserのid, nameが取得できてしまうという権限漏れが起きてしまいます。
修正したコード
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を使うと記述量は減らせます。 -
リクエスト時のクエリのネストとフィールドの上限を設定する
根本的な対策ではないですが、クエリのネストする深さとリクエストするフィールド数に上限を設定することで、万が一権限の実装が誤ってた場合の被害を最小限にできます。
class Schema < GraphQL::Schema
max_complexity 80
max_depth 8
end
最後に
-
GraphQLで定義した型に対して、このフィールドはアクセスできるがこのフィールドはアクセスできないという権限制御を漏れなく行おうとするとフィールド単位で権限を付ける必要が出ます
-
権限を実装する場合は関連経由だと取得できてしてしまわないかを気をつける必要があります
Supershipではプロダクト開発やサービス開発に関わる人を絶賛募集しております。
ご興味がある方は以下リンクよりご確認ください。
Supership株式会社 採用サイト
是非ともよろしくお願いします。