为什么一个控制器的Pundit策略会受到另一个控制器影响



我可能误解了Pundit的政策,但我面临着UserPolicySongPolicy冲突的问题。

如果UserPolicy中的语句被断言而忽略了SongPolicy:中所写的内容,会发生什么

Pundit::NotAuthorizedError in SongsController#edit 
not allowed to edit? this User
def authorization
authorize Current.user
end

这个问题是在为用户引入新角色后出现的,但我认为我可能没有正确配置它,并且出于某种原因,只有UserPolicy被视为在SongsController中声明授权?

我有两个控制器,用于检查要登录的用户(require_user_logged_in(,另一个用于检查Pundit的策略(authorization(:

class UsersController < ApplicationController
before_action :require_user_logged_in!, :authorization, :turbo_frame_check
# Actions were removed for brevity.
end
class SongsController < ApplicationController
before_action :require_user_logged_in!, :authorization, except: [:index, :show]
# Actions were removed for brevity.
end

authorization方法如下所示:

def authorization
authorize Current.user
end

有一个应用程序级策略类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

保护用户视图的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

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

不知道还能在这里尝试什么,我肯定我错过了一些东西,如果有人对Pundit有更多的了解,能让我知道他们对为什么一项政策的声明会泄露到另一项政策中的想法,那将非常有帮助。

您正在当前用户上调用authorize,该用户是User,因此Pundit将推断UserPolicy策略。除非您提供Song记录,否则它不会自动推断SongPolicy策略,即使您在SongController控制器中也是如此。

如果您想使用不同的策略,则需要通过authorize(policy_class:)提供。

authorize Current.user, policy_class: SongPolicy

像这样的隐式授权通常是一种代码气味。理想情况下,您应该针对当前用户上下文明确授权当前Song记录。

我认为您对Pundit的方法有误解。特别是您将授权的主题和授权的对象扭曲在一起。让我们试着解释一下。

您有一个参与者,他想对对象应用操作对象可能被授权接收操作

默认情况下,Pundit总是将参与者视为current_user

操作是策略上的一种方法。

对象是您正在处理的资源;在最琐碎的场景中,它可能是一个ActiveRecord对象,但它不必。

Pundit的authorize方法,用通俗的英语来说,是";授权资源bar从当前用户接收动作foo";。相反,你想做的是";授权当前用户在资源上应用操作foo

有什么区别?授权的主体和对象是交换的。IMO,在进行授权过程时,您应该回答以下问题:;该对象是否被参与者授权接收此操作">

object        action
------------  ------
authorize Current.user, :edit?

注意:参与者隐含地是current_user

注意:如果没有声明操作,那么它将隐式地为action_name

它解决了问题";该特定用户是否有权接收:编辑?来自当前用户">

根据推理,这是我认为适用于您的示例场景的正确方法:

class SongsController < ApplicationController
before_action :require_user_logged_in!, :authorization, except: [:index, :show]
private
def authorization
authorize Song
end
end

我不建议依赖回调,我宁愿写更明确的代码

def edit
@song = Song.find(params[:id])
authorize @song, :edit?
end

该代码解决了";这首特定的歌曲是否被授权接收:编辑?来自当前用户">

关于使用自定义policy_class的警告

authorize Current.user, policy_class: SongPolicy

使用此代码,将通过调用SongPolicy#edit?进行授权,但record将定期设置为Current.user的值;让我们假设有

class SongPolicy
def edit?
record.in_my_playlist?
end
end

其中in_my_playlist?Song的实例方法:您将结束

undefined method `in_my_playlist?` for #<User>

也许你没有在那里做你想做的事。

关于在逻辑中使用Current.user的警告

如果Current.user正在使用http://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html如果您的整个应用程序都依赖于该单例,那么您可能需要重新定义Pundit的默认用户,如所示

class ApplicationController < ActionController::Base
def pundit_user
Current.user
end
end

否则,您的业务逻辑和授权逻辑将依赖于两个可能不同的真相来源。

最新更新