使用graphql-ruby进行授权的方法

我认为,在使用GraphQL时,您可能希望在各种处理中进行授权。

    • このQueryはログインユーザーのみ実行できるようにしたい

 

    • このMutationは管理者のみ実行できるようにしたい

 

    • このQueryは自分の所有しているデータのときだけ返却するようにしたい

 

    このFieldは自分の所有しているデータのときだけ返却するようにしたい

由于我最初对graphql-ruby的知识非常有限,所以在获取和更新处理中调用了认证处理方法。但是,当我重新阅读graphql-ruby的文档时,我发现了一个用于授权的方法(authorized?),因此我决定兼作验证,并撰写了一篇文章。

2021年2月10日 补充记录

根据评论所提,似乎有一个名为”ready?”的类似方法。
这也在文档中有说明。
https://graphql-ruby.org/mutations/mutation_authorization.html

准备好了?和授权了?的区别在于它们在加载参数之前是否被调用。准备好了?在加载参数之前被调用,而授权了?似乎是在加载后被调用。

因此,如果在不使用参数检查的情况下,似乎使用”ready?”会更有效率。

关于graphql-ruby

这个Gem能够让你在Ruby(Rails)中轻松使用GraphQL。
https://github.com/rmosolgo/graphql-ruby

实际尝试试验许多细节才能真正知晓,但这份文档十分完整和出色。
https://graphql-ruby.org/guides

在我撰写这篇文章的时候,正在使用graphql: 1.11.1版本。
由于这个Gem还在不断进行版本升级,因此请注意如果版本不同可能会导致功能发生大的变化。

可接受的實施範例

我将解释第一个提到的四种模式的实现示例。

前提条件 (option 1)

我們將假設必要使用者登錄資訊已存儲在上下文中,由於認證不在本文的重點範疇內,故省略說明。

# ログインユーザーの情報はcontext[:current_user]に格納
# 未ログインの場合はnil
context = { current_user: current_user }

我希望只有登录用户才能执行此查询。

在这里,我们将实现一个查询,可以通过指定review_id返回相应的ReviewType。

在认可之前

在实施认可之前,我们需要实现获取ReviewType的查询。

module Types
  class QueryType < Types::BaseObject
    field :review, resolver: Resolvers::ReviewResolver
  end
end
module Resolvers
  class ReviewResolver < BaseResolver
    type Types::ReviewType, null: true

    argument :review_id, Int, required: true

    def resolve(review_id:)
      Review.find_by(id: review_id)
    end
  end
end
module Types
  class ReviewType < BaseObject
    field :id, ID, null: false
    field :title, String, null: true
    field :body, String, null: true
    field :secret, String, null: true
    field :user, Types::UserType, null: false
  end
end
module Types
  class UserType < BaseObject
    field :id, ID, null: false
    field :name, String, null: false
    field :email, String, null: false
  end
end
スクリーンショット 2020-07-24 13.51.33.png

实施认可

那么现在我们要在先前实施的处理中“仅限登录用户执行”的约束条件。

无需使用“authorized?”的实现。

在过去,我在resolve方法中添加了一个实现,在获取Review之前进行登录检查。

首先,我们将在BaseResolver中实现登录检查方法,以便可以从各种Resolver中使用。
如果context[:current_user]为空,则会引发错误。
顺便提一下,如果使用GraphQL::ExecutionError,它可以将异常转换为GraphQL错误格式,并进行响应。

 module Resolvers
   class BaseResolver < GraphQL::Schema::Resolver
     def login_required!
       # ログインしていなかったらraise
       raise GraphQL::ExecutionError, 'login required!!' unless context[:current_user]
     end
   end
 end

接下来,我们将在处理过程的开头调用BaseResolver的登录检查。

 def resolve(review_id:)
+  # 処理の最初にログインチェックを行う
+  login_required!

   Review.find_by(id: review_id)
 end
スクリーンショット 2020-07-25 12.53.22.png

在这种方法中,虽然可以实现所需的功能,但登录必需的解析器在处理开始时必须写入login_required!。
一直在寻找像控制器的before_action一样,在调用主处理之前自动进行授权的方法。

使用”authorized?”的实施方式

重新阅读了graphql-ruby的指南,我意识到了一个叫做authorized?的方法。
通过使用这个方法,在resolve方法之前可以进行授权,并且能够控制执行的可行性。
以下是在mutation中添加的指南,但也可以在Resolver中类似地进行添加。
https://graphql-ruby.org/mutations/mutation_authorization.html

由于需要登录的Resolver似乎是通用的,所以我创建了一个名为login_required_resolver的继承自需要登录的Resolver的类。参数(args)中的authorized?将存储与resolve相同的参数。

module Resolvers
  class LoginRequiredResolver < BaseResolver
    def authorized?(args)
      context[:current_user].present?
    end
  end
end

review_resolver将继承login_required_resolver进行修正。
其他实现与添加授权之前相同。

- class ReviewResolver < BaseResolver
+ class ReviewResolver < LoginRequiredResolver
スクリーンショット 2020-07-25 13.01.45.png

如果”authorized?”的结果为false,则只会返回”data: null”,不会返回错误信息。正如指南中所述,当”authorized?”为false时,只返回”data: null”是默认行为。如果返回”null”不会产生问题,则可以保持原样,但是如果未经授权,则尝试返回错误信息。

在中文中,要在”authorized?”函数内引发GraphQL::ExecutionError错误以添加错误信息是相当简单的。需要注意的是,如果不明确地返回true表示成功,则不会被认为是成功。

module Resolvers
  class LoginRequiredResolver < BaseResolver
    def authorized?(args)
      # 認可できない場合はGraphQL::ExecutionErrorをraise
      raise GraphQL::ExecutionError, 'login required!!' unless context[:current_user]

      true
    end
  end
end
スクリーンショット 2020-07-25 13.25.54.png

如果使用了 “authorized?”,在 “resolve” 方法中就不需要编写授权处理,因此可以更简洁地编写。(尽管本例的实现非常简单,因此差异并不大…)

我希望只有管理员能执行这个Mutation。

在这里,我们实现了一个通过指定review_id来更新相应评论标题和内容的Mutation。

在進行認可之前

在实施认可之前,我们将实施更新Review的Mutation。省略了之前使用的ReviewType等类。

module Types
  class MutationType < Types::BaseObject
    field :update_review, mutation: Mutations::UpdateReview
  end
end
module Mutations
  class UpdateReview < BaseMutation
    argument :review_id, Int, required: true
    argument :title, String, required: false
    argument :body, String, required: false

    type Types::ReviewType

    def resolve(review_id:, title: nil, body: nil)
      review = Review.find review_id
      review.title = title if title
      review.body = body if body
      review.save!

      review
    end
  end
end
スクリーンショット 2020-07-27 11.34.17.png

批准实施

在Mutation中,您可以像之前的例子一样使用authorized?函数。具体的使用方法请参考以下链接的指南:
https://graphql-ruby.org/mutations/mutation_authorization.html

创建一个只有管理员可以使用的Mutation继承自父类,并进行继承。

module Mutations
  class BaseAdminMutation < BaseMutation
    def authorized?(args)
      raise GraphQL::ExecutionError, 'login required!!' unless context[:current_user]
      raise GraphQL::ExecutionError, 'permission denied!!' unless context[:current_user].admin?

      super
    end
  end
end
- class UpdateReview < BaseMutation
+ class UpdateReview < BaseAdminMutation

对于Mutation的authorized属性返回False的情况,如果只返回False而不返回错误信息,那么数据将变为null,更新操作将无法执行。

虽然Resolver可能不会受到影响,但我认为如果Mutation没有返回错误信息就会不太清楚情况,因此我还是实现了返回GraphQL::ExecutionError的方法。

顺便说一下,根据指南的说明,似乎也可以通过在返回值中返回errors来返回错误信息。我尝试了一下,但是使用该方法无法返回locations和path,只能返回message。

如果只需要返回消息,那么无论使用哪种方法实现都应该是可以的。

def authorized?(employee:)
  if context[:current_user]&.admin?
    true
  else
    return false, { errors: ["permission denied!!"] }
  end
end
スクリーンショット 2020-07-27 11.48.29.png

只想要返回自己拥有的数据的这个查询。

在这里,我们将基于最初创建的”根据review_id指定相应ReviewType并返回的查询”进行改进。
最初创建的查询仅验证登录状态,而现在我们将添加检查Review是否属于该用户的检查。

尝试将登录检查与authorized?方法一起实现

如果能在登录检查中添加与authorized?相同的检查,那就好了,但是这次的检查必须在获取Review之后才能进行。
虽然在authorized?中也可以通过参数获取review_id来获取Review,但这样会使resolve的角色变得模糊。
我将实际进行实现。

 def authorized?(args)
   raise GraphQL::ExecutionError, 'login required!!' if context[:current_user].blank?

+   # この時点でreviewの取得が必要
+   review = Review.find_by(id: args[:review_id])
+   return false unless review
+   raise GraphQL::ExecutionError, 'permission denied!!' if context[:current_user].id != review.user_id

   true
 end

需要经过授权才能获取Review。
由于resolve方法也会获取Review,所以在这里再次获取可能效率低下。
那么,如果在resolve端实现检查呢?

 def resolve(review_id:)
-   Review.find_by(id: review_id)
+   review = Review.find_by(id: review_id)
+   raise GraphQL::ExecutionError, 'permission denied!!' if context[:current_user].id != review.user_id

+   review
 end

这个选择似乎比使用”authorized?”进行实现更有效率,但是通过将检查处理模块切出到”authorized?”中,导致在原本只包含数据获取处理逻辑的”resolve”中又加入了检查处理逻辑。

在最初,我认为只能在获得数据后才能进行检查的事情只能通过resolve进行检查,然而我意识到授权?(authorized?)也可以被定义在ReviewType中,所以我会尝试将其定义在ReviewType中。

使用ReviewType进行检查。

如果选择 ReviewType 进行检查,具体是什么意思?
我们将实际进行实施。

为了让所有人都能使用ReviewType,我希望创建一个名为MyReviewType的ReviewType,只有我才能查看的限制。

module Types
  class MyReviewType < ReviewType
    def self.authorized?(object, context)
      raise GraphQL::ExecutionError, 'permission denied!!' if context[:current_user].id != object.user_id

      true
    end
  end
end

请注意,正如指南中所述,使用Type的authorized?方法需要接受object和context作为参数。此外,由于这是一个类方法,需要特别注意。
更多信息请参考:https://graphql-ruby.org/authorization/authorization.html

只需将响应类型(Type)更改为MyReviewType即可,无需进行其他修改。

- type Types::ReviewType, null: true
+ type Types::MyReviewType, null: true
スクリーンショット 2020-07-27 22.40.15.png

这样就不需要在resolve方法中编写授权处理,因此可以简单地编写。另外,通过将响应设置为MyReviewType,只需读取模式定义,就可以明确地返回MyReviewType,表示“只能自己查看”,我认为这很好。

只有在登录用户自己的数据时才返回该字段。

在之前的例子中,我们定义了MyReviewType来限制只能查看自己的数据的整个回复。
然而,有时候我们可能只想限制查看特定的字段,而不是全部。

我想再次強調「ReviewType」。
在這裡,我們希望只有自己能夠查看”secret”欄位的數據。

module Types
  class ReviewType < BaseObject
    field :id, ID, null: false
    field :title, String, null: true
    field :body, String, null: true
    field :secret, String, null: true # <- これを自分の場合のみ見えるようにする
    field :user, Types::UserType, null: false
  end
end

阅读指南后发现可以在字段中实现authorized?,但是由于只定制一个字段似乎很困难,所以我决定在这里不使用authorized?进行实现。
有关字段的指南请点击此处。

如果定义了与字段名称相同的方法,那么将调用该方法。
我在那个方法里实现了授权。

module Types
  class ReviewType < BaseObject
    field :id, ID, null: false
    field :title, String, null: true
    field :body, String, null: true
    field :secret, String, null: true
    field :user, Types::UserType, null: false

    # field名のメソッドを定義すると呼び出される
    def secret
      # ログインユーザーとレビューを書いたユーザーが違う場合、nilを返却
      return if object.user_id != context[:current_user].id

      object.secret
    end
  end
end
スクリーンショット 2020-07-28 22.51.28.png

一旦将此检查实现在Resolver上,所有使用ReviewType的Resolver都必须考虑到secret的问题,但是如果将其实现在ReviewType上,各个独立的Resolver就不再需要考虑secret的访问控制。

最后

在开始使用graphql-ruby之前,我已经大致阅读了全部的指南,但是我错过了authorized?的存在…
除了authorized?之外,还有其他我还没注意到的方便功能。
而且,即使现在不存在,graphql-ruby也经常进行版本升级,所以未来可能还会添加新功能。我希望能继续关注graphql-ruby的发展动态。

广告
将在 10 秒后关闭
bannerAds