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.
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.
I will explain the implementation example of the first four patterns.
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 }
Here, "a query that returns the corresponding ReviewType by specifying review_id" is implemented.
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:
Now, let's add the restriction that "only the logged-in user can execute" to the process implemented earlier.
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.
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.
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.
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 ?.
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 ...)
Here, "Mutation that updates the title and body of the corresponding Review by specifying review_id" is implemented.
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.
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.
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.
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.
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.
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".
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.
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.
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