DEV Community

Cover image for Protect your GraphQL data with resource_policy
Povilas Jurčys
Povilas Jurčys

Posted on • Updated on

Protect your GraphQL data with resource_policy

GraphQL is a query language for APIs that was created by Facebook. It provides a more efficient, powerful, and flexible alternative to REST APIs. One of the main advantages of GraphQL is that it allows clients to request exactly the data they need, and nothing more.

In addition to its flexibility, GraphQL also provides an excellent way to handle authorization and access control. Authorization is the process of ensuring that a user has the proper permissions to perform an action, and access control refers to the methods and policies that regulate which users can access specific resources or data.

In this article, we will discuss how to integrate GraphQL with resource_policy, a Ruby gem for attribute-level authorization, to create fine-grained access control for your API. We will cover the basic concepts of resource_policy, how to use it to protect your resources, and how to expose your policies through GraphQL to provide your API users with visibility into which data they can access.

Two layers of protection

If you are interested in the authorization levels concept, I recommend checking my previous article titled Attribute level permission control in Rails where I delved into the topic of authorization and its two layers - action and attribute. In this article, I will focus on the implementation.

In simple terms, action authorization is a straightforward approach where a user's request is either accepted or rejected based on their role or other conditions. This approach is implemented in a controller through a simplified code such as:

class UsersController < ApplicationController
  before_action :require_admin, only: [:create]

  def show # everyone can do this action
    # ...
  end

  def create # only admins can do this action
    # ...
  end
end
Enter fullscreen mode Exit fullscreen mode

However, in attribute-level authorization, the system does not reject the request but rather returns only part of the requested data. For instance, different response bodies may be returned based on the user making the request.

// user(id: 1) response for admin
{
  "data": {
    "id": 1
    "email": "john@example.com"
  }
}

// user(id: 1) response for non-admin
{
  "data": {
    "id": 1
    "email": null // email is not authorized to see
  }
}
Enter fullscreen mode Exit fullscreen mode

Attribute-level authorization is more complex, but it is essential in more intricate applications. Luckily, there are tools like the resource_policy gem that make it easier to achieve this type of authorization.

What is resource_policy?

If you're looking for an easy way to configure attribute-level authorization for your Rails application, look no further than resource_policy. Based on the PolicyObject pattern, this gem offers several methods to make configuration simple.

In a PolicyObject class, for example, you can use the policy method to specify which attributes are allowed for which actions. In the following example, the id attribute can be read, and the email attribute can be read only if the current user is an admin:

class UserPolicy
  include ResourcePolicy::Policy

  policy do |c|
    c.attribute(:id).allowed(:read)
    c.attribute(:email).allowed(:read, if: :admin?)
  end

  def initialize(user, current_user)
    @user = user
    @current_user = current_user
  end

  def admin?
    @current_user.admin?
  end
end
Enter fullscreen mode Exit fullscreen mode

One of the benefits of resource_policy is that it's easy to integrate with other gems like a pundit, so you don't have to rewrite everything. If you're looking for a straightforward way to implement attribute-level authorization, resource_policy is definitely worth considering.

resource_policy and GraphQL

If you're using GraphQL and want to protect your data with a simple configuration, resource_policy provides a method called protected_resource. This method wraps your record and returns nil for every unauthorized attribute, making it easy to protect your data with a few lines of code.

For example, consider the following code:

current_user.admin? #=> false

user = User.find(1337)
user.id #=> 1337
user.email #=> "john.doe@example.com"

policy = UserPolicy.new(user, current_user)
protected_user = policy.protected_resource
protected_user.id #=> 1337
protected_user.email # nil
Enter fullscreen mode Exit fullscreen mode

In this example, protected_resource returns a user record with only the authorized attributes, which is useful for protecting data in GraphQL. With a few lines of code, you can configure resource_policy to protect your data in GraphQL:

class Types::QueryType < GraphQL::Schema::Object
  field :users, [UserType], 'Returns protected users'

  def users
    current_user = context[:current_user]
    Users.all.map do |user|
      UserPolicy.new(user, current_user).protected_resource
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

By using UserPolicy.new(user, current_user).protected_resource, you can wrap the user record with the policy and return only the authorized attributes to the client. This makes it easy to add attribute-level authorization to your GraphQL API with minimal effort.

Querying policies itself

Expressing authorization rules can be a bit challenging with the use of other authorization gems, such as pundit or cancancan. The resource_policy gem provides a more concise and expressive policy definition that uses a simple block-based syntax that makes it easy to understand and write authorization rules for each attribute.

One of the benefits of resource_policy is that you have easy access to the protected attributes list through user_policy.attributes_policy.all_allowed_to(:read). This feature provides fine-grained control over attribute access by specifying different access levels, such as :read or :write.

With these features, resource_policy is a great option if you want to expose policies through GraphQL or an API. Exposing policies is important because it helps the frontend or any other heavy GraphQL user know which data they can access before making any real request. To give an idea, GraphQL can look like the following query:

query userPolicy(id: 123) {
  email {
    isReadable
    isWritable
  }

  name {
    isReadable
    isWritable
  }
}
Enter fullscreen mode Exit fullscreen mode

This query returns something like the following, based on the policy result:

{
  "data": {
    "userPolicy" {
      "user" {
        "email": {
          "isReadable": true
          "isWritable": false
        },
        "name": {
          "isReadable": true
          "isWritable": true
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This way, the GraphQL user can decide whether to render input fields that are read-only or skip making requests altogether.

Implementation details

You might be wondering how to implement resource_policy with GraphQL. While the library does not come with built-in GraphQL support, it can be easily integrated with GraphQL using a few lines of code.

To get started, you'll need to create an AttributePolicyType that exposes the access levels for each attribute. In the simplest case, you might expose only read and write levels:

class AttributePolicyType < GraphQL::Schema::Object
  field :is_writable, GraphQL::Types::Boolean, property: :writable?
  field :is_readable, GraphQL::Types::Boolean, property: :readable?
end
Enter fullscreen mode Exit fullscreen mode

You'll also need to create a policy type for each model. For example, you can create a UserPolicyType to expose the data from the UserPolicy:

class UserPolicyType
  UserPolicy.policy.attributes.each_key do |attribute|
    field attribute, AttributePolicyType
  end  
end
Enter fullscreen mode Exit fullscreen mode

This class defines attributes dynamically based on the policy, so you don't need to hard-code them.

Finally, you can update the Query type to make the policy accessible via GraphQL:

class Query < GraphQL::Schema::Object
  # ...
  field :user_policy, UserPolicyType do
    argument :id, ID
  end

  def user_policy(id:)
    user = User.find(id)
    UserPolicy.new(user, context[:current_user])
  end
end
Enter fullscreen mode Exit fullscreen mode

Now you can query the policy using query userPolicy(id: 1) { ... } to find out which attributes are accessible. This can be useful for front-end developers who want to know which data they can access before making any requests.

Final thoughts

In conclusion, resource_policy is a powerful and flexible gem that can help you easily implement attribute-level authorization in your Ruby on Rails application. Its concise and expressive policy definition, easy access to protected attributes, and fine-grained control over attribute access make it a strong contender for exposing policies via GraphQL or API.

By following the steps outlined in this article, you can create an AttributePolicyType and UserPolicyType, and update your Query GraphQL type to expose your policies. This will enable your GraphQL users to know which data they have access to before making any real requests, thus helping them make better decisions and reduce unnecessary network traffic.

While resource_policy does not have out-of-the-box GraphQL support, it can be easily integrated with GraphQL with just a few lines of code. With resource_policy, you can ensure that your application's data is protected and accessible only by authorized users, providing a more secure and reliable user experience.

Top comments (0)