CanCanCan中尝试将自定义动作别名到真实动作read时的报错问题及解决方案咨询
问题背景与代码
我在使用CanCanCan定义项目权限时,遇到了一个权限别名的问题。先贴出我的ProjectsAbility代码:
module Abilities class ProjectsAbility < Ability def abilities can :manage, Project, user_id: user.id if user.is?(:super_admin) can :read, Project end if user.is?(:district_admin) can [:read, :manage_details, :download], Project, user: { district: { id: user_district_ids } } end if user.is?(:school_admin) can [:read, :manage_details, :download], Project, user: { school_id: user_school_ids || user.district.school_ids } end if user.is?(:advisor) can [:read, :manage_details, :download], Project, user: { advisors: { id: user.id } } end # 尝试但报错的别名写法 # alias_action :manage_details, :download, to: :read end end end
可以看到,district_admin、school_admin和advisor这三个角色的权限高度重复,都需要read、manage_details和download这三个动作的权限。为了简化代码,我想通过别名让拥有read权限的用户自动获得另外两个自定义动作的权限,这样就不用在控制器的每个动作里重复写authorize!,也不用在能力类里重复列出三个动作。
但当我尝试注释里的写法时,直接抛出了错误:
CanCan::Error
You can't specify target (read) as alias because it is real action name
后来我试了另一种写法,把三个动作都别名到一个自定义动作上,是可以正常工作的:
alias_action :read, :manage_details, :download, to: :view_manage_details
然后给对应角色赋予:view_manage_details的权限即可。但我还是想知道,有没有办法直接把manage_details和download别名到原生的read动作上?或者是不是需要做额外处理才能实现这个需求?
问题原因分析
其实这个报错的原因很明确:CanCanCan的alias_action机制不允许把动作别名到框架原生的动作(比如read、create、update这些),因为原生动作是内置的核心动作,已经有固定的权限检查逻辑,框架不允许将其他动作“挂靠”到原生动作上,避免逻辑冲突。
可行解决方案
虽然不能直接把自定义动作别名到read,但我们有两种方式可以实现你的核心需求——简化权限定义,避免重复代码:
方案1:自定义动作别名(推荐,符合框架规范)
既然不能指向原生的read,我们可以反过来,把read和两个自定义动作一起别名到一个新的自定义动作上,然后统一给角色赋予这个自定义动作的权限。优化后的代码如下:
module Abilities class ProjectsAbility < Ability def abilities # 先定义别名:把manage_details、download都归到read_access下,同时read也关联它 alias_action :read, :manage_details, :download, to: :read_access can :manage, Project, user_id: user.id if user.is?(:super_admin) can :read_access, Project end if user.is?(:district_admin) can :read_access, Project, user: { district: { id: user_district_ids } } end if user.is?(:school_admin) can :read_access, Project, user: { school_id: user_school_ids || user.district.school_ids } end if user.is?(:advisor) can :read_access, Project, user: { advisors: { id: user.id } } end end end end
然后在控制器里,不管是show(对应read)、manage_details还是download动作,只需要写一行权限检查:
authorize! :read_access, @project
这样就能一次性覆盖三个动作的权限检查,完全达到你想要的简化效果。
方案2:合并权限条件,批量赋予动作
如果你不想用自定义别名,也可以通过any_of条件合并的方式,一次性给三个动作赋予相同的权限:
module Abilities class ProjectsAbility < Ability def abilities can :manage, Project, user_id: user.id # 收集所有拥有read类权限的角色条件 read_permission_conditions = [] read_permission_conditions << {} if user.is?(:super_admin) read_permission_conditions << { user: { district: { id: user_district_ids } } } if user.is?(:district_admin) read_permission_conditions << { user: { school_id: user_school_ids || user.district.school_ids } } if user.is?(:school_admin) read_permission_conditions << { user: { advisors: { id: user.id } } } if user.is?(:advisor) # 一次性给三个动作赋予合并后的权限条件 can [:read, :manage_details, :download], Project, any_of: read_permission_conditions end end end
这种方式不需要修改控制器的authorize!调用——比如你在manage_details动作里写authorize! :manage_details, @project,会自动匹配对应的权限条件;read动作的authorize! :read, @project也能正常工作。
总结
CanCanCan限制将动作别名到原生动作是为了保护内置逻辑的稳定性,所以最简洁且符合框架设计的方式还是方案1(自定义动作别名),既能大幅简化能力类的代码,也能统一控制器的权限检查调用。
备注:内容来源于stack exchange,提问作者mohsin_jamshaid

