fbpx

Blogs from the Ranch

< Back to Our Blog

Authorizing jsonapi_resources, Part 1: Visibility

The JSON API web service specification is growing in popularity and adoption, and in Rails the jsonapi_resources gem is a great option for creating conforming web services. The gem handles a lot of the grunt work for you, but it can also make it hard to know how to do common tasks like controlling access to resources based on authorization rules.

Let’s take a look at how to implement authorization for your resources. In this first post, we’ll do it directly in the resources itself, and in the second part, we’ll using the popular pundit gem.

As an example, we’ll be working on a small web service for tracking video games. You can clone the finished web service from GitHub, as well as the client app we’ll be using in the tutorial.

Let’s log in as user josh. He’s entered a few of his favorite video games into the system. He follows user actionplayer but doesn’t follow sportplayer. The web service doesn’t perform any authorization checks yet, so josh can currently view and modify video games for all three users:

Games for all players

Ultimately, josh wants to see actionplayer’s awesome games, but he shouldn’t be able to edit them. And he doesn’t want to see sportplayer’s repetitive football games—that’s why he’s not following sportplayer!

Restricting Users To Their Own Records

To start out, let’s restrict users to interacting only with their own games—that is, games that are directly associated to them. In the next post, we’ll give them access to other users’ games, but this is a simple place to start. When I say “interacting with,” I mean viewing, creating, editing and deleting: at this point, users shouldn’t be able to perform any of these actions on other users’ video games.

We can accomplish all this by defining a VideoGameResource.records class method, which jsonapi_resources uses when it wants to retrieve the list of VideoGames. The default implementation of records in the parent class retrieves every VideoGame in the database, but if we override it in VideoGameResource we can restrict which records are visible. That list will also be used when looking up a VideoGame by ID to show, edit or delete. As a result, excluding a video game from the records method will also prevent it from being edited or deleted, because it will not be found.

The implementation of VideoGameResource.records is pretty straightforward:

app/resources/video_game_resource.rb

 class VideoGameResource < ApplicationResource
   attributes :title

   has_one :user
+
+  def self.records(options = {})
+    user = current_user(options)
+    user.video_games
+  end
 end

We’re not quite ready to test our app out yet, though, because we implemented the records method in terms of a current_user helper method that doesn’t exist yet! So let’s build one.

This current_user method won’t be able to get to the user directly—it needs to be in the resource, and the user is only accessible from within controllers. But jsonapi_resources provides a context object for passing data from the controller to the resource. We’ll use this to pass the current user along.

First let’s implement the current_user method, retrieving the user from the context. Let’s put it on the parent ApplicationResource class, since it’s something we’d want available for any future resources we might create:

app/resources/application_resource.rb

 class ApplicationResource < JSONAPI::Resource
   abstract
+
+  private
+
+  def self.current_user(options)
+    options.fetch(:context).fetch(:current_user)
+  end
 end

Next we need to update the controller to store the current user in the context, so that it will be available where the resource looks for it. The convention in jsonapi_resources is for there to be a context method on the controller, so let’s define one. We’ll put this on the parent class for sharing as well:

app/controllers/application_controller.rb

 class ApplicationController < JSONAPI::ResourceController
   before_action :doorkeeper_authorize!
+
+  private
+
+  def context
+    {current_user: current_user}
+  end
+
+  def current_user
+    @current_user ||= User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token
+  end
 end

This context method will be called, and the value it returns will be passed by jsonapi_resources into the resource, which will then retrieve the user and use it to find only the video games belonging to that user.

Now that we’ve implemented everything the records method needs, when josh logs in, he can only view, edit, and delete video games directly associated with him:

Josh's games

If you’re following along locally, give it a try at this point to see your progress!

Restricting Users to Creating Records For Themselves

However, we still have the problem that users can create records for other users. This happens because we require them to set the user relationship when creating a VideoGame, and they can easily set it to a user other than themselves.

Let’s fix this by making the user field read-only and, when creating a record, automatically setting it to the current user. We can make the field read-only by excluding it from creatable_fields and updatable_fields:

app/resources/video_game_resource.rb

   attributes :title

   has_one :user

+  def self.creatable_fields(context)
+    super - [:user]
+  end
+
+  def self.updatable_fields(context)
+    super - [:user]
+  end
+
   def self.records(options = {})
     current_user(options).video_games
   end

Now that the user field is read-only, we need to automatically set it to the current user when creating a record. We can do that using a before_create callback:

app/resources/video_game_resource.rb

   attributes :title

   has_one :user

+  before_create { _model.user = current_user }
+
   def self.creatable_fields(context)
     super - [:user]
   end

Note that we’re calling current_user without an options parameter this time—here’s why. The self.current_user(options) method we previously created works from within class methods, but the before_create callback is run on an instance of VideoGameResource, not the class. From within an instance, the user needs to be retrieved slightly differently—among other things, it doesn’t have an options parameter available. As a result, we need to create a different version of the current_user helper for instances—one without an options parameter. Let’s create that new helper method now, putting it in the parent class again for the sake of reuse:

app/resources/application_resource.rb

   private

+  def current_user
+    context.fetch(:current_user)
+  end
+
   def self.current_user(options)
     options.fetch(:context).fetch(:current_user)
   end

Now users have access only to their own records, which they can create, read, update and delete. In the next post, we’ll give them read-only access to users they follow.

Not Happy with Your Current App, or Digital Product?

Submit your event

Let's Discuss Your Project

Let's Discuss Your Project