[RAILS] How to authorize using graphql-ruby

When using GraphQL, you may want to authorize it in various processes.

Initially, I had little knowledge of graphql-ruby, so I called the process to authorize in the acquisition and update process, but when I reread the graphql-ruby document again, the method for authorization (authorized?) I found out that there was something, so I wrote an article to verify the operation.

About graphql-ruby

It is a Gem that makes GraphQL easy to use in Ruby (Rails). https://github.com/rmosolgo/graphql-ruby

The details are often not known until you actually try them, but the documentation is great. https://graphql-ruby.org/guides

At the time of writing this article, I'm using graphql: 1.11.1. Please note that the operation of Gem is still upgraded, so if the version is different, the operation may have changed significantly.

Implementation example of authorization

I will explain the implementation example of the first four patterns.

Prerequisites

It is assumed that the login user information required for authorization is stored in the context. Authentication is not the main point of this article, so I will omit the explanation.

app/controllers/graphql_controller.rb


#Login user information is context[:current_user]Store in
#Nil if not logged in
context = { current_user: current_user }

I want this query to be run only by logged-in users

Here, "a query that returns the corresponding ReviewType by specifying review_id" is implemented.

Before getting approval

Implement a query that gets the ReviewType before implementing authorization.

app/graphql/types/query_type.rb


module Types
  class QueryType < Types::BaseObject
    field :review, resolver: Resolvers::ReviewResolver
  end
end

app/graphql/resolvers/review_resolver.rb


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

app/graphql/types/review_type.rb


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

app/graphql/types/user_type.rb


module Types
  class UserType < BaseObject
    field :id, ID, null: false
    field :name, String, null: false
    field :email, String, null: false
  end
end

When executed in GraphiQL, it looks like this: スクリーンショット 2020-07-24 13.51.33.png

Implement authorization

Now, let's add the restriction that "only the logged-in user can execute" to the process implemented earlier.

Implementation without authorized?

Earlier I had an implementation that checks the login before getting a Review with the resolve method.

First, implement a login check method in BaseResolver so that it can be used from various Resolvers. If context [: current_user] is not included, an error will occur. By the way, if you use GraphQL :: ExecutionError, the response will be converted to GraphQL error format just by raising.

app/graphql/resolvers/base_resolver.rb


 module Resolvers
   class BaseResolver < GraphQL::Schema::Resolver
     def login_required!
       #Raise if you are not logged in
       raise GraphQL::ExecutionError, 'login required!!' unless context[:current_user]
     end
   end
 end

Then call the BaseResolver login check at the beginning of the process.

app/graphql/resolvers/review_resolver.rb


 def resolve(review_id:)
+  #Perform a login check at the beginning of the process
+  login_required!

   Review.find_by(id: review_id)
 end

If you run it with GraphiQL without logging in, it will be as follows. スクリーンショット 2020-07-25 12.53.22.png

I've achieved what I want to do with this method, but Resolvers that require login must always write login_required! At the beginning of the process. I've been looking for a way to automatically authorize this process before it's called, like before_action in controller.

Implementation using authorized?

When I read the graphql-ruby guide again, I noticed that there is a method called authorized ?. It seems that you can use this to perform authorization before the resolve method and control whether it can be executed or not. Below is a guide to add to mutation, but you can add it to Resolver as well. https://graphql-ruby.org/mutations/mutation_authorization.html

Since Resolver that requires login seems to be usable for general purposes, I created login_required_resolver that Resolver that requires login inherits. The parameters (args) of authorized? contain the same parameters as resolve.

app/graphql/resolvers/login_required_resolver.rb


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

Modify review_resolver to inherit login_required_resolver. Other implementations are the same as before adding the authorization.

app/graphql/resolvers/review_resolver.rb


- class ReviewResolver < BaseResolver
+ class ReviewResolver < LoginRequiredResolver

If you run it with GraphiQL without logging in, it will be as follows. スクリーンショット 2020-07-25 13.01.45.png

If the result of authorized? is false, there is no error information and only data: null is returned. As mentioned in the guide, if authorized? Is false, it seems that the default behavior is to return only data: null. If there is no problem with the specification of returning null, you can leave it as it is, but if it is not authorized, try changing it so that error information is also returned.

Adding error information is easy and can be done by raising GraphQL :: ExecutionError in authorized ?. By the way, if you succeed, you need to be careful because it will not be recognized as success unless you explicitly return true.

app/graphql/resolvers/login_required_resolver.rb


module Resolvers
  class LoginRequiredResolver < BaseResolver
    def authorized?(args)
      #GraphQL if not authorized::Raise ExecutionError
      raise GraphQL::ExecutionError, 'login required!!' unless context[:current_user]

      true
    end
  end
end

If you run it with GraphiQL without logging in, it will be as follows. Now you can return the error information even if you use authorized ?. スクリーンショット 2020-07-25 13.25.54.png

If you use authorized ?, you can write it simply because you don't need to write the authorization process in the resolve method. (This example is a fairly simple implementation, so there is not much difference ...)

I want this Mutation to be run only by the administrator

Here, "Mutation that updates the title and body of the corresponding Review by specifying review_id" is implemented.

Before entering the authorization

Implement a Mutation that updates the Review before implementing the authorization. Classes that are used as they are, such as ReviewType used in the previous example, are omitted.

app/graphql/types/mutation_type.rb


module Types
  class MutationType < Types::BaseObject
    field :update_review, mutation: Mutations::UpdateReview
  end
end

app/graphql/mutations/update_review.rb


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

When executed in GraphiQL, the Review data is updated as follows. スクリーンショット 2020-07-27 11.34.17.png

Implement authorization

You can use authorized? In Mutation as in the previous example. It is listed in the guide below. https://graphql-ruby.org/mutations/mutation_authorization.html

Create a parent class that Mutation inherits, which is only available to administrators, and inherit it.

app/graphql/mutations/base_admin_mutation.rb


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

app/graphql/mutations/update_review.rb


- class UpdateReview < BaseMutation
+ class UpdateReview < BaseAdminMutation

If Mutation authorized? Also only returns false, error information will not be returned, data will be null, and update processing will not be executed. Resolver still looks good, but Mutation doesn't understand unless it returns error information, so I implemented it to raise GraphQL :: ExecutionError as well. By the way, if you read the guide, there seems to be a way to return error information by returning errors as the return value as shown below. I tried, but the following method did not return the locations and paths under errors, but I was able to return the messages of errors. If you only need to return the message, you can implement it by either method.

def authorized?(employee:)
  if context[:current_user]&.admin?
    true
  else
    return false, { errors: ["permission denied!!"] }
  end
end

When executed by a user who does not have administrator privileges in GraphiQL, it will be as follows. Of course, in case of an error, the update process will not be executed. スクリーンショット 2020-07-27 11.48.29.png

I want this query to be returned only when the data I own

Here, we will modify it based on the "query that returns the corresponding ReviewType by specifying review_id" that was created first. The first thing I made was checking only the login status, but this time Review is my property? Add a check for.

Try to implement it in the same authorized? As login check

It would be nice if I could add a check to the same authorized? As the login check, but this check can only be checked after getting Revew. Even with authorized ?, review_id is received as an argument, so it is possible to get Review, but doing so obscures the role of resolve. I will actually implement it.

app/graphql/resolvers/login_required_resolver.rb


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

+   #You need to get a review at this point
+   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

You will need to get a Review with authorized ?. Since it is also acquired by the resolve method, it seems inefficient to acquire it here as well. So what about implementing a check on the resolve side?

app/graphql/resolvers/review_resolver.rb


 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

This seems to be more efficient than implementing with authorized ?, but by cutting out the check process in authorized ?, the check process has been added to resolve, which only described the data acquisition process.

At first, I thought that the only thing that can be checked only after data acquisition is to check with resolve, but I learned that authorized? Can also be defined in ReviewType, so I will define it in ReviewType.

Check with Review Type

What does it mean to check with ReviewType? I will actually implement it.

I want to make ReviewType available to anyone, so I will create a ReviewType called MyReviewType with restrictions that only I can view.

app/graphql/types/my_review_type.rb


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

As mentioned in the guide, authorized? Used in Type takes object and context as arguments. Also, since it is a class method, you need to be careful. https://graphql-ruby.org/authorization/authorization.html

All you have to do is set the response type to MyReviewType. No other modifications are required.

app/graphql/resolvers/review_resolver.rb


- type Types::ReviewType, null: true
+ type Types::MyReviewType, null: true

If you specify a Review other than yourself in GraphiQL, it will be as follows. スクリーンショット 2020-07-27 22.40.15.png

Now that you don't have to write the authorization process in the resolve method, you can write it simply. Also, by setting the response to MyReviewType, just reading the schema definition will make it clear that this query returns MyReviewType = "only you can view it".

This Field is returned only when the logged-in user's own data

In the previous example, I defined MyReviewType so that I can only see the entire response for my data. However, there may be times when you want to hide only certain fields, not all.

I will repost the Review Type. Here I want the secret column to only see my data.

app/graphql/types/review_type.rb


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 # <-Make this visible only for you
    field :user, Types::UserType, null: false
  end
end

If you read the guide, it seems that authorized? Can be implemented in the field as well, but it seems difficult to customize only one field, so I decided to implement it without using authorized? Here. https://graphql-ruby.org/authorization/authorization.html Click here for a field guide https://graphql-ruby.org/fields/introduction.html#field-parameter-default-values

If you define the same method as the field name as shown below, that method will be called. I implemented authorization within that method.

app/graphql/types/review_type.rb


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

    #Called when defining a method with field name
    def secret
      #If the logged-in user and the user who wrote the review are different, return nil
      return if object.user_id != context[:current_user].id

      object.secret
    end
  end
end

If you specify a Review other than yourself in GraphiQL, it will be as follows. The secret has been returned null. スクリーンショット 2020-07-28 22.51.28.png

If you implement this check in Resolver, all Resolvers that use ReviewType will have to consider secret, but by implementing it in ReviewType, individual Resolvers will not have to think about access control of secret.

Finally

I thought I had read through the guide before I started using graphql-ruby, but I overlooked the existence of authorized? ... It seems that there are other useful functions other than authorized? That you haven't noticed yet. Also, even if it doesn't exist now, the version has been upgraded, and there is a high possibility that new functions will be added in the future, so I would like to continue to check the trends of graphql-ruby.

Recommended Posts

How to authorize using graphql-ruby
How to build CloudStack using Docker
How to execute a contract using web3j
How to sort a List using Comparator
[Rails] How to upload images using Carrierwave
[Java] How to calculate age using LocalDate
How to deploy
[Swift5] How to implement animation using "lottie-ios"
How to implement image posting using rails
How to make asynchronous pagenations using Kaminari
[Rails] How to handle data using enum
How to insert icons using Font awesome
How to output Excel and PDF using Excella
[Rails] How to create a graph using lazy_high_charts
How to delete a controller etc. using a command
How to play audio and music using javascript
[Ethereum] How to execute a contract using web3j-Part 2-
How to implement the breadcrumb function using gretel
[Rails] How to upload multiple images using Carrierwave
How to generate a primary key using @GeneratedValue
How to link images using FactoryBot Active Storage
[Java] How to operate List using Stream API
How to develop OpenSPIFe
How to call AmazonSQSAsync
How to use Map
How to use rbenv
How to use letter_opener_web
How to use with_option
How to use fields_for
How to use java.util.logging
How to use collection_select
How to use Twitter4J
How to use active_hash! !!
How to install Docker
How to use MapStruct
How to use hidden_field_tag
How to use TreeSet
How to write dockerfile
How to uninstall Rails
How to install docker-machine
[How to use label]
How to make shaded-jar
How to write docker-compose
How to use identity
How to use hashes
How to write Mockito
How to create docker-compose
How to install MySQL
How to write migrationfile
How to build android-midi-lib
How to use Dozer.mapper
How to use Gradle
How to use org.immutables
How to use VisualVM
How to use Map
How to install ngrok
How to type backslash \
How to concatenate strings
How to figure out how much disk Docker is using
How to unit test with JVM with source using RxAndroid
How to make an oleore generator using swagger codegen