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.
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
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
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
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
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
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
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
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. 🙏
Top comments (0)