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?
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 aGET 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:
- set current authenticated user Like this:
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 !
Top comments (0)