You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 12:05:13