为何SongsController的Pundit验证触发UserPolicy而非SongPolicy?
Pundit策略串用问题:SongController触发UserPolicy授权错误
在SongsController的edit操作中触发Pundit::NotAuthorizedError,提示“not allowed to edit? this User”——系统直接套用了UserPolicy的断言逻辑,完全忽略了SongPolicy规则。该问题在新增用户角色后出现,怀疑是配置错误导致SongsController授权时仅检查UserPolicy而非对应SongPolicy。
相关代码
1. 控制器配置
UsersController
class UsersController < ApplicationController before_action :require_user_logged_in!, :authorization, :turbo_frame_check # Actions were removed for brevity. end
SongsController
class SongsController < ApplicationController before_action :require_user_logged_in!, :authorization, except: [:index, :show] # Actions were removed for brevity. end
2. authorization方法定义
def authorization authorize Current.user end
3. ApplicationPolicy基类
# frozen_string_literal: true class ApplicationPolicy attr_reader :user, :params, :record # Allows params to be part of policies. def initialize(context, record) if context.is_a?(Hash) @user = context[:user] @params = context[:params] else @user = context @params = {} end @record = record end def index? false end def show? false end def create? false end def new? create? end def update? false end def edit? update? end def destroy? false end class Scope def initialize(user, scope) @user = user @scope = scope end def resolve raise NotImplementedError, "You must define #resolve in #{self.class}" end private attr_reader :user, :scope end end
4. UserPolicy定义
class UserPolicy < ApplicationPolicy class Scope < Scope end def index? user.has_role?(:admin) end def show? # Access if admin or the same user only. user.has_role?(:admin) || is_same_user? end def create? index? end def new? create? end def update? index? || is_same_user? end def edit? update? # This is called when accessing a view for `SongsController`. end def destroy? index? || is_same_user? end def delete? destroy? end private # Used to keep a user from editing another. # Admins should be allowed to edit all users. def is_same_user? # Check if user being accessed is the one being logged in. params[:id].to_s == Current.user.username.to_s end end
5. SongPolicy定义
class SongPolicy < ApplicationPolicy class Scope < Scope end def index? end def show? end def create? user.has_role?(:admin) || user.has_role?(:collaborator) # This is ignored. end def new? create? end def update? create? end def edit? create? end def destroy? user.has_role?(:admin) end def delete? destroy? end end
问题根源与修复方案
问题根源
核心问题出在通用的authorization方法里:authorize Current.user每次都传入User实例,Pundit会根据传入对象的类型自动匹配对应Policy(也就是UserPolicy),和当前所在控制器无关。所以在SongsController执行授权时,实际触发的是UserPolicy的edit?规则,而非SongPolicy。
修复方案
方案1:控制器单独绑定对应资源授权
移除ApplicationController中的通用authorization方法,在每个控制器内单独处理:
UsersController调整
class UsersController < ApplicationController before_action :require_user_logged_in!, :set_user, :authorization, :turbo_frame_check private def set_user @user = User.find(params[:id]) end def authorization authorize @user end end
SongsController调整
class SongsController < ApplicationController before_action :require_user_logged_in!, :set_song, :authorization, except: [:index, :show] private def set_song @song = Song.find(params[:id]) end def authorization authorize @song end end
方案2:动态匹配控制器对应资源
如果想保留通用授权方法,可在ApplicationController中根据控制器名称动态获取对应资源:
def authorization resource_name = controller_name.singularize resource = instance_variable_get("@#{resource_name}") authorize resource if resource end
前提是每个控制器需在before_action中提前设置对应实例变量(比如SongsController的@song、UsersController的@user)。
额外注意事项
- SongPolicy中的
index?、show?方法为空,会继承ApplicationPolicy的false,若需开放这两个接口,需明确返回true或自定义权限逻辑; - UserPolicy的
is_same_user?方法用params[:id]与Current.user.username对比,需确认params[:id]确实为用户名格式,避免类型不匹配导致权限判断错误。
内容的提问来源于stack exchange,提问作者csalmeida
相关产品推荐
相关产品推荐

