From aef2f1899199d48d0e8214527ba79f0e7075e20b Mon Sep 17 00:00:00 2001 From: Tad Thorley Date: Mon, 29 Jun 2026 18:05:01 -0600 Subject: [PATCH] Docs: use idiomatic Pundit/CanCanCan authorization recipes The Pundit recipe hardcoded the policy class (UserPolicy.new); use Pundit.policy! so the record drives policy lookup, matching how a controller resolves it. The CanCanCan recipe built Ability.new inside the action; inject current_ability instead, since can?/authorize! already delegate to it. Switch the example resource from User to Project so the actor (current_user) and the authorized object are visually distinct. --- docs/integration-patterns.md | 38 +++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/docs/integration-patterns.md b/docs/integration-patterns.md index 9a8edc7..861d94d 100644 --- a/docs/integration-patterns.md +++ b/docs/integration-patterns.md @@ -188,14 +188,14 @@ end ## Authorization -Authorization gems work naturally with action classes. Inject the current user from the controller and call the authorization check during orchestration. +Authorization gems work naturally with action classes. Inject the authorization context from the controller -- the current user for Pundit, the current ability for CanCanCan -- and run the check during orchestration. ### Pundit -Call the [Pundit](https://github.com/varvet/pundit) policy directly inside the action: +Let [Pundit](https://github.com/varvet/pundit) infer the policy from the record with `Pundit.policy!`, then call the query. It returns a plain boolean, so a denial flows through your formatter as a `Forbidden` response -- no `rescue_from`, no exceptions: ```ruby -class Users::DestroyAction +class Projects::DestroyAction include ActionFigure[:jsend] params_schema do @@ -203,52 +203,54 @@ class Users::DestroyAction end def call(params:, current_user:) - user = User.find(params[:id]) - unless UserPolicy.new(current_user, user).destroy? - return Forbidden(errors: { base: ["not authorized to delete this user"] }) + project = Project.find(params[:id]) + unless Pundit.policy!(current_user, project).destroy? + return Forbidden(errors: { base: ["not authorized to delete this project"] }) end - user.destroy! + project.destroy! NoContent() end end ``` ```ruby -class UsersController < ApplicationController +class ProjectsController < ApplicationController def destroy - render Users::DestroyAction.call(params:, current_user: current_user) + render Projects::DestroyAction.call(params:, current_user: current_user) end end ``` +`Pundit.policy!(current_user, project)` does the same policy-class lookup as a controller (`project` → `ProjectPolicy`); you name the query (`:destroy?`) explicitly, since an action class has no controller `action_name` to infer it from. + ### CanCanCan -With [CanCanCan](https://github.com/CanCanCommunity/cancancan), build the ability from the current user: +With [CanCanCan](https://github.com/CanCanCommunity/cancancan), inject the controller's `current_ability` and consult it directly. CanCanCan's `can?` and `authorize!` helpers already delegate to `current_ability`, so passing the ability keeps the action idiomatic and decoupled from how the ability is built: ```ruby -class Users::DestroyAction +class Projects::DestroyAction include ActionFigure[:jsend] params_schema do required(:id).filled(:integer) end - def call(params:, current_user:) - user = User.find(params[:id]) - if Ability.new(current_user).can?(:destroy, user) - user.destroy! + def call(params:, current_ability:) + project = Project.find(params[:id]) + if current_ability.can?(:destroy, project) + project.destroy! NoContent() else - Forbidden(errors: { base: ["not authorized to delete this user"] }) + Forbidden(errors: { base: ["not authorized to delete this project"] }) end end end ``` ```ruby -class UsersController < ApplicationController +class ProjectsController < ApplicationController def destroy - render Users::DestroyAction.call(params:, current_user: current_user) + render Projects::DestroyAction.call(params:, current_ability:) end end ```