Eager loading @current_user to save time on pundit policies

nodefiend

chowderhead

Posted on June 13, 2018

Eager loading @current_user to save time on pundit policies

The Problem:

If your rails controllers have the ability to run batch Create/read/update/delete, how can you quickly run policy checks without bogging down your server?

Photo by Alberto Joel Filipe

This is article is not about installing pundit, if you are curious about set up , click here

The Context:

  • Our Controllers are Search/save/delete , if you do not pass search an id it will assume you are running a GET ALL and it will return a collection of database objects.
  • we have Pundit Policies set up for each database model/controller.
  • each policy extends a base policy as well.
  • all of our controllers also extend a base api controller.

Lets say we want to get a list of projects that our user is a part of:

frontend_api_request.js


//pass it an empty object for a standard get all request to the controller.
const ApiRequestSearch = async (data) => {
    const response = await fetch('https://yourapi.com/projects',data);
    const json = await response.json();
}

ApiRequestSearch({data:{});

When it gets to the backend it hits our base controller:

base_controller.rb

class Api::ApiController < ActionController::Base
    include Pundit
    before_action :set_current_user
    ...
end

base_controller_helper.rb
Our base controller uses a helper to:

def set_current_user
  User.current = current_user    
end

  • instantiate pundit policy (let me know if you know a better way)
def yourPolicy
  "#{controller_name.classify}Policy".constantize
end

When our front end request is received, first it hits our base search method , where we pass the data , and the current user into our pundit policy.

base_controller.rb

def search
...
    #If the policy returns false, we return a 403 to the front end.
    if ! self.yourPolicy.new(current_user, @collection).search?
        ApiErrors.raise_custom(message: 'You are not authorized to search!',     
        status: 403)
    end
...
end

now we hit the base application policy where we initialize all the stuff we will need for the to run our policy checks, also if we just want to run a generic search policy check, we can add it here

base_policy.rb

class ApplicationPolicy
  attr_reader :user, :record, :collection

  def initialize(user, record)
    @user   = user
    @record = record
    @collection = record
  end

So if wanted to make our requests super laggy and inefficient , we might do something like this : (and yes, this was my first idea -.-)

projects_policy.rb

class ProjectPolicy < ApplicationPolicy
# BAD ! DONT DO IT
    def search?
        @collection.map do |project|
            return true if project.user_id == @current_user.id 
            return false
        end
    end
end

How can we do this better?

Instead lets use eager loading (ahead of time) to get all of our associations we need before hand, so we don't have to spend time checking each object in the array.

Lets go back to where set up our current user, and add a class variable for current user with the loaded associations.

base_controller_helper.rb

def set_current_user
  User.current = current_user
  # eager load the associations we need 1
  # time, so we don't need to do it later
  @current_user = User.includes(:projects).find(current_user.id) if current_user
end

setting this will give us a user model with associations loaded up.
so that when we hit our base_policy.rb we can use the associations and check against them, without having to make additional queries , or mapping through and running a check for each object.

Lets go back to our base policy and pass in our new @current_user :

base_controller.rb

def search
...
    #If the policy returns false, we return a 403 to the front end.
    if ! self.yourPolicy.new(@current_user, @collection).search?
        ApiErrors.raise_custom(message: 'You are not authorized to search!',     
        status: 403)
    end
...
end

Now inside of our policy we can do something like this :

projects_policy.rb

class ProjectPolicy < ApplicationPolicy
# GOOD ! DO IT
    def search?
        # get all IDS of the projects current user can access
        current_user_project_ids = @current_user.projects.pluck(:id)
        # get the ids of the collection that the user is searching
        collection_ids = @collection.pluck(:id)
        #compare both arrays, if empty , return true! 
        (collection_ids - current_user_project_ids).empty?
    end
end

So Array comparison , like this is much faster then making additional queries, or mapping through a list of array items right?

If you have any improvement suggestions , or i just made silly mistakes , please let me know below in the comments , im totally open for constructive criticism!

thanks for reading !

💖 💪 🙅 🚩
nodefiend
chowderhead

Posted on June 13, 2018

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related