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)