DEV Community

Dale Zak
Dale Zak

Posted on • Updated on

Using CanCanCan With StimulusReflex In Your Rails App

If you are using CanCanCan for authorization and also want to use the magic of StimulusReflex for reactive page updates, these strategies will help you check user abilities in your reflexes.

stimulus_reflex_cancancan

CanCanCan is a powerful authorization library that allows you to authorize! the current_user for an action, as well as restrict records only accessible_by their current_ability.

def index
 authorize! :index, Classroom
 @classroom = Classroom.accessible_by(current_ability)
end
Enter fullscreen mode Exit fullscreen mode

Once you start using StimulusReflex, you’ll soon need to utilize the accessible_by in your reflexes to only obtain records permitted for the current_user as well. The following are strategies how to do this for both selector morphs and page morphs.


Selector Morphs With CanCanCan

For selector morphs, you have two options for using CanCanCan’s accessible_by in your reflex.

Option 1: create new ability for user

First delegate the current_user to your connection, then create a new ability passing that into the accessible_by call.

class ClassroomsReflex < ApplicationReflex
 delegate :current_user, to: :connection
 def change_school
  if element.value.present?
   user_ability = Ability.new(current_user)
   school = School.find(element.value)
   classrooms = school.classrooms.accessible_by(user_ability).order(:name)
  else
   school = nil
   classrooms = []
  end
  morph #classrooms”, render(partial: “/classrooms/classrooms”, locals: { school: school, classrooms: classrooms })
 end
end
Enter fullscreen mode Exit fullscreen mode

Option 2: delegate current_ability to controller

Another technique is to delegate the current_ability from the controller, passing that into the accessible_by call.

class ClassroomsReflex < ApplicationReflex
 delegate :current_ability, to: :controller

 def change_school
  if element.value.present?
   school = School.find(element.value)
   classrooms = school.classrooms.accessible_by(current_ability).order(:name)
  else
   school = nil
   classrooms = []
  end
  morph #classrooms”, render(partial: “/classrooms/classrooms”, locals: { school: school, classrooms: classrooms })
 end
end
Enter fullscreen mode Exit fullscreen mode

Note, one of the reasons selector morphs are faster than page morphs is because they don’t need to instantiate the controller. This means that the above will increase the response time adding 10–25ms hit on every reflex. So Option 1 is recommended when possible, although there might be some scenarios where this is still useful.


Page Morphs With CanCanCan

For page morphs, you can not delegate current_ability to the controller, due to the fact that both StimulusReflex and CanCanCan instantiate the controller internally. This causes an issue with the instance variables set in your reflex always being nil in the controller afterwards. So you have two options for page morphs instead.

Option 1: create new ability for user

Similar to the selector morph, you can delegate to current_user, then create a new ability and pass it into accessible_by.

class ClassroomsReflex < ApplicationReflex
 delegate :current_user, to: :connection

 def change_school
  if element.value.present?
   user_ability = Ability.new(current_user)
   @school = School.find(element.value)
   @classrooms = @school.classrooms.accessible_by(user_ability).order(:name)
  else
   @school = nil
   @classrooms = []
  end
 end
end
Enter fullscreen mode Exit fullscreen mode

Option 2: move accessible_by calls to controller

Another option is to move the accessible_by calls out of the reflex and into the controller. This is not a very StimulusReflex-y way, although there are some scenarios where this could suffice so still worth noting.

class ClassroomsReflex < ApplicationReflex
 def change_school
  if element.value.present?
   @school = School.find(element.value)
  else
   @school = nil
  end
 end
end
Enter fullscreen mode Exit fullscreen mode
class ClassroomsController < ApplicationController
 def index
  authorize! :index, School
  authorize! :index, Classroom
  @school ||= School.find(params[:school_id)
  @schools ||= School.accessible_by(current_ability).order(:name)
  @classrooms ||= @school.present? ? @school.classrooms.accessible_by(current_ability).order(:name) : []
 end
end
Enter fullscreen mode Exit fullscreen mode

Model Based CanCanCan Abilities

If you’ve transitioned to using separate abilities per model, then the good news is Option 1 will work even better for you!

class ClassroomsReflex < ApplicationReflex
 delegate :current_user, to: :connection

 def change_school
  if element.value.present?
   classroom_ability = ClassroomAbility.new(current_user)
   @school = School.find(element.value)
   @classrooms = @school.classrooms.accessible_by(classroom_ability).order(:name)
  else
   @school = nil
   @classrooms = []
  end
 end
end
Enter fullscreen mode Exit fullscreen mode

In this case, rather than creating abilities for all models you are only creating abilities for the Classroom model. This can especially help if you are using _ids queries in your abilities, since those other abilities won’t be executed. If you are interested in separate abilities per model, I’d recommend reading Lazy Load CanCanCan Abilities In Rails.


For more information regarding using CanCanCan with StimulusReflex, visit authentication section on the official documentation.

Big thanks to @theleastbad, @RogersKonnor and @marcoroth_ for helping debug the issues I was having using CanCanCan with StimulusReflex, which was the source of these above strategies. 🙏

Discussion (0)