Rails 多对多关系模型的Cancancan授权

Rails Cancancan authorization of models in many-to-many relation

我通过 UserRoleAssoc 在多对多关系中拥有 UserRole 模型在 Ruby-上-Rails.

我需要一个页面(Web 界面),用户可以从该页面(网络界面)add/delete 与用户关联的角色,普通用户和管理员可以在其中 编辑 自己的角色只有.

我的问题是如何实施该方案,特别是授权。

这里是 UserRole 的模型(只是标准的多对多):

class User < ApplicationRecord
  has_many :user_role_assocs, dependent: :destroy
  has_many :roles, through: :user_role_assocs
end

class Role < ApplicationRecord
  has_many :user_role_assocs
  has_many :users, through: :user_role_assocs
end

class UserRoleAssoc < ApplicationRecord
  belongs_to :user
  belongs_to :role
end

根据 DHH 的原则(参见 Jerome Dalbert 的“How DHH Organizes His Rails Controllers”),这些操作应该像控制器一样实现,比如说,ManageUserRolesController,执行一个或多个 CRUD 操作.在这种情况下,ManageUserRolesController createdelete 中的一个或两个 UserRoleAssoc 上的多个记录。

由于 Web 用户界面应该允许人们从 URL 一次性管理角色列表(带有 select 框),我制作了 create 方法ManageUserRolesController 两者都做,在 params 中接收用户 ID (user) 和一组角色 ID (roles)(不过我愿意接受建议!) . routes.rb如下:

resources :manage_user_role, only: [:create]  # index may be provided, too.

现在,为了将一个用户限制为 add/delete 对任何其他用户的角色,我想在 models/ability.rb 中写一些类似的东西,连同一个控制器:

 # models/ability.rb`
can :create, ManageUserRoles, :PARAMS => {user: user}  # "PARAMS" is invalid!! Any alternative ideas?
can :manage, ManageUserRoles if user.administrator?

 # controllers/manage_user_roles_controller.rb
class ManageUserRolesController < ApplicationController
  load_and_authorize_resource
end

似乎可以按照“Passing params to CanCan in RoR" and CanCan wiki的答案中描述的方式实现它,虽然我认为必须定义与控制器对应的模型以指向非标准table , 在 models/manage_user_role.rb

class ManageUserRole < ApplicationRecord
  self.table_name = 'user_role_assocs'
end

但这看起来很别扭……
'Rails'实现多对多模型授权的方式(Version 6+)是什么?具体来说,对于具有某些约束的用户 add/delete 多个角色来说,什么是好的界面?

注意路由不一定要像上面的示例代码那样;可以设置路由,以便用户 ID 作为路径的一部分传递,例如 /manage_user_role/:user_id 而不是通过 params,只要授权有效。

这是一个答案,我最后使用的解决方案。

背景

根据定义,多对多关系很复杂,我认为没有适合所有情况的简单解决方案。当然,CanCanCan 中的 Ability 默认不支持它(除非你做一些复杂的黑客攻击,比如 OP 想要避免的方式,如问题中所述)。

然而,在这种特殊情况下,OP 想要处理的情况是基于用户 ID 的约束,这基本上是一对多(或 has_many)关系,即一个用户拥有多个角色。然后,它实际上可以按照 Cancancan/Ability 的标准方式进行工作。

一般来说,OP的用户和角色之间存在多对多关系的情况有三种处理方式(,每个用户可以有多个角色和一个角色可能属于多个用户):

  1. 像在用户(控制器)模型中一样处理它,
  2. 像在角色(控制器)模型中一样处理它,
  3. 或者UserRoleAssoc(Controller),也就是User和Role之间jointable关联的一个model(n.b.,这个Controller是默认情况下不会创建,因此如果您使用它,您必须手动创建它)。

让我在下面讨论三个中哪一个最符合Cancancan授权的目的。

Cancancan 如何使用 Ability 进行授权以及什么最适合这种情况

对于默认的 CRUD 操作,Cancancan 处理 can 语句如下(以我的理解);这基本上是对 official reference of Cancancan:

这种情况的简要总结
  • 对于动作index,除了动作类型index之外,Cancancan仅有的信息是用户,模型Class(with/without范围) .所以,基本上,Cancancan 不会也不能做太多。重要的是,与 can 语句关联的 Ruby 块(如果有)是 调用的。
  • 如果模型的(主要)ID在路径中给出,即对于showeditupdatedestroy、Cancancan的动作从数据库中检索模型,并将其提供给您使用 can 语句提供的算法,如果给定,包括 Ruby 块。

在 OP 的情况下,用户不应被授权处理除 her/himself 以外的任何其他用户的角色。那么,就必须根据current_user中的一个和path/route中给出的两个user-id进行判断。为了Rails自动从路径中拾取后者,必须相应地设置路由。

然后,因为“ID”是一个User-ID,处理这种情况最自然的解决方案是使用UsersController(上面描述的情况1);那么包含在默认路由中的ID被Rails和Cancancan解释为User#id。相反,如果采用情况 2,路径中的默认 ID 将被解释为 Role#id,这不适用于这种情况。至于case 3(问题中提到的),UserRoleAssoc#id只是给一个协会的随机数,与User#idRole#id无关。所以,也不适合这种情况。

解决方案

如上所述,必须仔细选择控制器的操作,以便 Cancancan 根据路径中给定的 ID 正确设置用户。

OP 提到控制器的 createdelete (destroy)。从技术上讲,在这种情况下,所需的操作是 createdelete 用户和角色之间的新关联之一或两者。然而,在Rails'默认路由中,create不带ID参数(当然不是,因为ID是在DB创建时给定的!)。因此,create 的动作名称在这种情况下并不合适。 update 最合适。在自然语言中,我们将其解释为用户的(角色关联)状态将是 update-d 与 Controller 的此操作。 update的默认HTTP方法是PATCH/PUT,也符合操作的意思。

最后,这是我找到的解决方案(使用 Rails 6.1):

routes.rb
resources :manage_user_roles, only: [:update]
  # => Route: manage_user_role PATCH  /manage_user_roles/:id(.:format)  manage_user_roles#update
manage_user_roles_controller.rb
class ManageUserRolesController < ApplicationController
  load_and_authorize_resource :user
  # This means as far as authorization is concerned,
  # the model and controller are User and UsersController.
  
  my_params = params.permit('add_role_11', 'del_role_11', 'add_role_12', 'del_role_12')
end
查看(提交数据)

这可以在用户的​​ show.html.erb 或其他任何地方。

<%= form_with(method: :patch, url: manage_user_role_path(@user)) do |form| %>
  Form components follow...
app/models/ability.rb
def initialize(user)
  if user.present?
    can :update, User, id: user.id
  end
end

我认为,关键是简化案例。尽管多对多关系本质上是复杂的,但您最好将每种情况分成更小、更简单的片段来处理。然后,他们可能会毫不费力地适应现有的方案。