Four Key Reasons to Learn Markdown
Back-End Leveling UpWriting documentation is fun—really, really fun. I know some engineers may disagree with me, but as a technical writer, creating quality documentation that will...
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:
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
!
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 VideoGame
s. 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:
If you’re following along locally, give it a try at this point to see your progress!
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.
Writing documentation is fun—really, really fun. I know some engineers may disagree with me, but as a technical writer, creating quality documentation that will...
Humanity has come a long way in its technological journey. We have reached the cusp of an age in which the concepts we have...
Go 1.18 has finally landed, and with it comes its own flavor of generics. In a previous post, we went over the accepted proposal and dove...