fbpx

Blogs from the Ranch

< Back to Our Blog

Authorizing jsonapi_resources, Part 2: Policies

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.

Accessing Records for Followed Users

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 VideoGames. 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:

Games for self and users followed

Refactoring to Use 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 VideoGameResources. 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 VideoGames 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:

Making Other Users’ Records Read-Only

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:

Josh trying to delete another user's game

Conclusion

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 user
  • JSONAPI::Resource#before_ callbacks to set fields and perform authorization checks
  • JSONAPI::ResourceController.context to pass data from the controller to the resource

Thankfully, 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.

Not Happy with Your Current App, or Digital Product?

Submit your event

Let's Discuss Your Project

Let's Discuss Your Project