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...
In the last post on authorizing jsonapi_resources, we began looking into adding authorization rules to jsonapi_resources
web services. We worked on a sample web service for tracking video games, and got to the point where user josh
could only see and edit his own records. Today, we’ll improve our web service by adding permission for josh
to view (but not edit) games belonging to the user he’s following.
As a reminder, if you want to follow along in the code, you can clone the finished web service from GitHub, as well as the client app we’ll be using in the tutorial.
Let’s start with making games belonging to followed users available to josh
, and afterward we can make them read-only.
Rather than code this logic directly on the resource, let’s define an Active Record scope on the VideoGame
model to encapsulate the concept of “games for a user and the users they follow”:
app/models/video_game.rb
class VideoGame < ApplicationRecord
belongs_to :user
+
+ scope :for_user_and_followed, ->(user) {
+ user_ids = [user.id] + user.following.pluck(:id)
+ where(user_id: user_ids)
+ }
end
Now we can update the VideoGameResource.records
class method, which jsonapi_resources
uses when it wants to retrieve the list of VideoGame
s. In part 1 we implemented it to return all of the current user’s records, but now let’s change it to use the scope we just created:
app/resources/video_game_resource.rb
end
def self.records(options = {})
user = current_user(options)
- user.video_games
+ VideoGame.for_user_and_followed(user)
end
end
Now josh
can see games belonging to him and to actionplayer
, whom he follows. He can’t see games for sportplayer
, whom he doesn’t follow:
pundit
There’s one problem: at this point, not only can josh
see actionplayer
’s games, he can also edit and delete them. We need to change the games to be read-only.
This requirement adds a new dimension of complexity to our authorization. Up until now, we’ve only had rules about visibility, but now we also have to have “policies:” rules about what actions can be taken. Implementing both visibility and policy rules directly in VideoGameResource
would add too many responsibilities to that class—it should stay focused on representing the resource, not restricting access to it.
Instead, let’s use the popular pundit
gem to create a separate class exclusively focused on authorizing VideoGameResource
s. To begin, we add gem 'pundit'
to our Gemfile and run bundle install
. Then we run rails generate pundit:install
to set up some base classes, and pundit
is ready to go. (If you’re following along and running a Rails application locally, restart your server at this point so Rails will load classes from the new app/policies
directory.)
Next, we’ll refactor our code to move our existing visibility rules into what pundit
calls a policy scope. A pundit
policy scope is implemented slightly differently from an Active Record scope, but the purpose is the same: both are reusable query conditions to retrieve records matching certain criteria. A pundit
policy scope is specifically for retrieving only the records the user is authorized to see. This is exactly what we were doing in the VideoGameResource.records
method, so all we need to do is move that code into a policy scope.
The pundit
policy scope for VideoGame
s needs to be implemented as a Scope
class inside a VideoGamePolicy
class. Let’s create these two classes, deriving each from a corresponding base class generated by the pundit:install
command:
app/policies/video_game_policy.rb
+class VideoGamePolicy < ApplicationPolicy
+ class Scope < ApplicationPolicy::Scope
+ end
+end
Next, we create a Scope#resolve
method, which is what pundit
will call to retrieve the list of records to show. Previously our VideoGameResource.records
method was responsible for this querying, but now Scope#resolve
will be, so we copy the query code from the former to here:
app/policies/video_game_policy.rb
class VideoGamePolicy < ApplicationPolicy
class Scope < ApplicationPolicy::Scope
+ def resolve
+ scope.for_user_and_followed(user)
+ end
end
end
The only change we made is to retrieve the VideoGame
class via the scope
method, rather than hard-coding it. This requires a bit of explanation: how does scope
know to return VideoGame
, and what’s the point of using it instead of hard-coding?
The reason to add this indirection is that it allows our policy scope class to be composable with other query conditions. For example, if we were to add a search feature in the future, we would want to apply the policy scope to the list of search results, retrieving only the records in the search results that are visible to the user. A pundit
policy scope handles this by allowing us to pass in a set of records (technically, an Active Record model class or relation) that we can retrieve with scope
and then applying policy scoping to. Even though we’ll just be passing in VideoGame
for now, avoiding hard-coding sticks with pundit
conventions and makes our Scope class more flexible for the future.
Now that we’ve set up this policy scope, let’s use it in place of the code we copied out of VideoGameResource.records
. Instead of instantiating it directly, we use a pundit
helper method:
app/resources/video_game_resource.rb
def self.records(options = {})
user = current_user(options)
- VideoGame.for_user_and_followed(user)
+ Pundit.policy_scope!(user, VideoGame)
end
end
With this, our refactor to pundit
is complete. We’re still letting users access video games that belong to them or users they follow, but we’re now doing so using pundit
, which prepares us for the next step:
Now we want to restrict a user from being able to edit or delete other users’ video games. To do this, let’s define the necessary pundit
policy methods: an update?
and destroy?
method that indicate that those actions are only allowed if the video game is owned by the user. Since the logic is the same in each case, we can define an owned?
method once, and alias it so update?
and destroy?
will call owned?
:
app/policies/video_game_policy.rb
class VideoGamePolicy < ApplicationPolicy
+
+ def owned?
+ record.user == user
+ end
+
+ alias_method :create?, :owned?
+ alias_method :update?, :owned?
+ alias_method :destroy?, :owned?
+
class Scope < ApplicationPolicy::Scope
def resolve
user_ids = [user.id] + user.following.pluck(:id)
Note that we also added a corresponding create?
method. Right now we don’t strictly need it, but wiring one up to the resource now might avoid confusion later if we add a create?
method to the policy and wonder why it’s not being called by the resource.
At the point we want authorization checks to happen, we need to call the pundit
authorize
method, which in turn will call the appropriate one of the policy methods we just defined. In jsonapi_resources
, a convenient place to check authorization is in before_...
callbacks on our resource:
app/resources/video_game_resource.rb
before_create :set_user
+ before_create { authorize(_model, :create?) }
+ before_update { authorize(_model, :update?) }
+ before_remove { authorize(_model, :destroy?) }
+
def self.creatable_fields(context)
super - [:user]
end
Note that the name of the jsonapi_resources
callback is before_remove
but the policy method is destroy?
, not remove?
. The callback method needs to be before_remove
, but the policy method name can be anything you like. I could have just as easily called it remove?
, but I chose to call it destroy?
to match Rails and pundit
defaults.
One more quick change we need to make: to make the authorize
method available to the resource, we need to include Pundit
. Let’s do that once again on the parent class for reusability:
app/resources/application_resource.rb
class ApplicationResource < JSONAPI::Resource
+ include Pundit
+
abstract
private
The policies are now hooked up, and josh
will be prevented from editing or deleting actionplayer
’s records. But instead of a nice friendly error message, he gets a 500 Server Error
saying “Internal Server Error: not allowed to update?” and “delete?”. A 500 error usually means “the server failed to fulfill an apparently valid request,” but in this case it’s not a server failure. The JSON API spec (1.0 version) agrees: it states that “A server MUST return 403 Forbidden
in response to an unsupported request to update a resource or relationship.” Let’s update the code to render a 403 HTTP status instead.
We add a handler in our ApplicationController
to render a :forbidden
HTTP status whenever it sees a Pundit::NotAuthorizedError
:
app/controllers/application_controller.rb
class ApplicationController < JSONAPI::ResourceController
before_action :doorkeeper_authorize!
+ rescue_from Pundit::NotAuthorizedError, with: Proc.new { head :forbidden }
private
def context
However, if we retry our bad request, we still get a 500 error! This is because, by default, jsonapi_resources
swallows errors, logs them and renders a 500 HTTP status. To allow our pundit
error to bubble up to our controller to be handled there, we need to create an initializer for jsonapi_resources
and add Pundit::NotAuthorizedError
to an exception whitelist:
config/initializers/jsonapi_resources.rb
+JSONAPI.configure do |config|
+ config.exception_class_whitelist = [Pundit::NotAuthorizedError]
+end
We’ll need to restart our local Rails server again for this initializer to be picked up. Then, when we rerun our bad request, we get the 403 error we expect.
Our web service now has all the authorization rules in place that we want: josh
can view only the games belonging to him and to actionplayer
, the user he follows. He can edit and delete his own games, but not games belonging to actionplayer
. Even if the UI incorrectly shows edit/delete functionality for actionplayer
’s video games, the server correctly prevents those modifications, returning an error:
Although jsonapi_resources
requires us to implement authorization differently than we’re used to in Rails, it’s not too hard to adjust. It just requires familiarizing ourselves with the methods and communication mechanisms to customize the behavior of jsonapi_resources
:
JSONAPI::Resource.records
to define which records are visible for a given current userJSONAPI::Resource#before_
callbacks to set fields and perform authorization checksJSONAPI::ResourceController.context
to pass data from the controller to the resourceThankfully, pundit
is flexible enough that it can be used within resources as well as Rails controllers. (It’s pretty cool that pundit
works for this unforeseen use case because it’s not tightly coupled to Rails controllers!) Using it together with jsonapi_resources
allows you to easily create secure web services, while keeping your authorization logic nicely separated from your resource definitions.
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...