Drupal 10:群组中添加、管理和理解自定义权限的综合指南

Drupal 10:为团体添加自定义权限

Drupal 里的“团体”(Group)模块是个强大的工具,它能把用户和内容归集成一个单一实体。在 Drupal 站点中,这些任意组合的实体集合可用于站点内部的编辑团队、公司订阅服务(用户可自行管理),或者任何需要创建用户团体的场景。

在最近一个使用“团体”模块的 Drupal 开发项目中,为完成特定任务,我公司对团体权限系统做了深入研究。该权限系统和 Drupal 中现有的权限系统类似,不同的是,这里的权限总是在团体和用户之间,以及一个可选的附加实体之间建立联系。要是想创建一些用户团体,让他们能访问团体内部的页面和其他实体,那么这种基于团体的本地权限设置就非常实用。

不过,团体权限可不简单。权限系统中不同层次的设置,可能会让人难以判断是什么因素阻止了对某个特定实体的访问。而且,大部分文档和第三方模块是围绕“团体”模块的 2 版本构建的,而当前发布的是 3 版本,这使情况变得更复杂。例如,有个关于扩展团体访问控制的文档页面,但它仅适用于“团体”模块的 2.0 版本,对最新版本没什么帮助。

在本文中,我公司将探讨如何在“团体”模块中创建和使用权限,以便在用户属于某个团体时授予他们特定的权限。随着文章推进,每个示例会逐渐变复杂,我公司还会展示如何在站点中使用这些权限来控制访问。

首先,快速了解一下团体权限级别是如何工作的,这会很有帮助。

一、团体权限级别如何工作

团体权限模型和 Drupal 核心权限系统协同工作,并对其进行了扩展,让每种团体类型都能有不同的权限模型。这意味着,虽然用户可能拥有一个 Drupal 用户角色,但他们在团体(或团体内部的项目)中能执行的操作,取决于他们在团体中所拥有的权限,以及他们是否为团体成员。

以下是用户和团体所涉及的不同权限级别的层次结构:

  • Drupal 角色:正常的 Drupal 用户角色赋予你在团体系统中执行某些操作的权限。例如,创建新团体就是基于 Drupal 的权限,因为此时还没有成员系统。
  • 外部人员角色:该角色定义了一个没有团体成员身份,但在 Drupal 中拥有某种角色的人。你可以配置团体,允许特定角色的用户对团体实体内部的内容有更多访问权限。这是一个可选角色,必须进行配置才能生效。
  • 内部人员角色:这是团体的成员,同时也拥有特定的 Drupal 角色。如果用户在团体内拥有成员身份,那么即便其成员身份不允许某些操作,他们的 Drupal 角色仍可赋予他们在团体内部的额外权限。同样,这也是一个可选角色,必须进行配置才能生效。
  • 成员身份:与团体关联的用户会被授予成员身份。这种成员身份附带了一组特定的权限。
  • 带有角色的成员身份:也可以在团体内部创建角色,为用户提供在团体内部执行操作的不同访问级别。默认情况下,团体会提供一个“管理员”角色,但你可以添加更多角色来微调权限。

由此可见,这里需要考虑多个不同的权限级别。如果没有仔细思考,很容易陷入混乱。在团体权限层次解释文档页面中,对这个简单概述有更详细的解释。

在团体设置中,我公司常见的一个问题是,所创建的一组权限导致:当站点管理员账户加入一个团体时,其在团体内的权限比非成员时 更少。这是因为该角色使用了内部人员角色的权限集,而该角色的权限与他们的外部人员角色所赋予的访问权限不同。

在设置团体时,你可以选择将某些 Drupal 角色映射到团体内部的权限。虽然这非常实用,并且通常也是你希望站点运行的方式,但如果你想避免访问权限方面的困扰,确保正确配置这一点至关重要。

考虑到这些,我们来看看如何创建一个简单的团体权限。

二、创建一个简单的团体权限

为团体创建权限最简单的方法,是在自定义模块中使用一个 x.group.permissions.yml 文件。这与普通的 Drupal 权限类似,只是仅与团体相关。

例如,在一个名为 custom_group_permissions 的模块中创建了以下代码片段,因此创建的权限文件为 custom_group_permissions.group.permissions.yml

  
编辑团体标题:
  标题: '编辑团体标题'
  描述: '赋予成员编辑团体标题的能力。'
  

这将创建一个名为“编辑团体标题”的权限,我们为其传递了标题和描述,这些将显示在团体权限页面上。团体权限还有更多可用选项,但这是入门的最简单方法。

添加此文件后,我们可以在所有团体类型的团体权限页面中看到以下选项。

在这里,我们仅允许团体管理员以及拥有管理员角色的用户(包括团体外部和内部的管理员)编辑团体标题。

为了让这个权限发挥作用,我们需要创建一个钩子来改变团体编辑表单的工作方式。每个团体实体都有一个名为 hasPermission() 的方法,用于检查用户对团体的权限。

以下代码实现了 hook_form_FORM_ID_alter() 钩子,并针对“group_activity_edit_form”,该表单用于编辑类型为“活动”的团体。

  
/**
 * 实现 hook_form_FORM_ID_alter() 钩子。
 */
function custom_group_permissions_form_group_activity_edit_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
  // 修改 “活动” 团体类型的 group_edit_form 表单。
  $group = $form_state->getFormObject()->getEntity();
  if ($group instanceof \Drupal\group\Entity\Group) {
    $account = \Drupal::currentUser();
    if ($group->hasPermission('编辑团体标题', $account) === FALSE) {
      $form['label']['widget']['#disabled'] = TRUE;
    }
  }
}
  

有了这段代码,假设我们也赋予该用户更新团体本身的能力,那么他们要更改团体标题(在内部称为“标签”),还必须被授予“编辑团体标题”的权限。在导出该团体的配置时,你也可以看到该角色的权限。

(一)权限参数

我们已经在团体权限的 YAML 文件中看到了团体权限的标题和描述参数,但还有哪些其他权限属性可供我们使用呢?“团体”模块中的 \Drupal\group\Access\GroupPermissionHandlerInterface 接口将以下列表定义为可用的属性。

  • 标题:权限的未翻译的可读名称,将显示在权限管理页面上。你可以像在 t() 中一样使用占位符。
  • 标题参数:(可选)标题的占位符值。
  • 描述:(可选)对该权限作用的未翻译描述。你可以像在 t() 中一样使用占位符。
  • 描述参数:(可选)描述的占位符值。
  • 限制访问:(可选)一个布尔值,设置为 TRUE 时表示站点管理员应将此权限的访问限制在可信任的用户范围内。这应该用于在各种潜在用例中存在固有安全风险的权限。设置为 TRUE 时,权限管理页面上的该权限旁边将显示一条标准警告消息。默认为 FALSE。
  • 警告:(可选)在权限管理页面上为该权限显示的未翻译警告消息。此警告将覆盖因“限制访问”设置为 TRUE 而自动生成的警告。这种情况应很少使用,因为所有权限都应有一个清晰、一致的安全警告,且在整个站点保持一致。请使用“描述”键来提供与你定义的权限相关的特定信息。你可以像在 t() 中一样使用占位符。
  • 警告参数:(可选)警告的占位符值。
  • 允许用于:(可选)一个字符串数组,定义哪些成员类型可以使用此权限。可能的值包括:'匿名用户'、'外部人员'、'成员'。如果留空,将默认包括所有三种类型。
  • 提供者:(可选)权限的提供者名称。默认为提供该权限的模块。你可以将其设置为另一个模块的名称,使其看起来像是该模块提供的权限。
  • 部分:(可选)权限的未翻译部分名称。这用于在权限表单上保持清晰的概述。对于插件提供的权限,默认为插件名称;对于其他权限,默认为“通用”。
  • 部分参数:(可选)部分名称的占位符值。
  • 部分标识:(可选)用于标识该部分的机器名称,对于插件提供的权限,默认为插件 ID;对于其他权限,默认为“通用”。

请注意,此处传递的所有字符串都是未翻译的,因为它们都伴随着一个“_args”属性,允许你将参数传递给字符串的翻译过程。例如,在为团体权限部分准备显示时,“标题”属性会像这样更新。

  
$permission['标题'] = $this->t($permission['标题'], $permission['标题参数']);
  

要在 YAML 文件中定义这个,可以这样做。

  
编辑团体标题:
  标题: '编辑团体 @参数'
  标题参数: {
    '@参数': '标题'
  }
  描述: '赋予成员编辑团体标题的能力。'
  

对于权限数组中需要为字符串传递参数的其他部分,也会进行类似的处理。

(二)路由上的团体权限

除了添加钩子并直接检查权限,你还可以在路由定义中添加 _group_permission_group_member 要求。以下代码片段展示了在路由定义中使用这些要求的示例。

  
custom_group_permissions.example:
  路径: '/group_reports/{group}'
  默认值:
    _标题: '团体报告'
    _控制器: '\Drupal\custom_group_permissions\Controller\ReportController::report'
  要求:
    _权限: '管理内容'
    _团体权限: '访问团体报告页面'
    _团体成员: 'TRUE'
  

有了这些设置,Drupal 将检测到这些要求,并为该路由添加以下权限检查。

  • _权限 要求是一个标准的 Drupal 权限检查,这意味着我们首先检查该用户是否具有“管理内容”的权限,才能查看此内容。
  • _团体权限 要求确保用户具有“访问团体报告页面”的团体权限。请记住,考虑到内部人员和外部人员的权限,此权限并不一定意味着该用户是团体成员。
  • _团体成员 要求确保用户是当前加载的团体的成员,该团体通过 URL 中的“{group}”参数传递给路由。

如果这些权限检查中有任何一项返回权限禁止的结果,那么该路由将不会对用户显示。此处检查的顺序很重要,这意味着我们首先允许广泛的权限,然后随着检查的进行逐步缩小权限范围。在上述代码中,我们首先运行 _权限 检查,接着是 _团体权限 检查,最后是 _团体成员 检查。

另外,如果你想为现有的路由添加团体权限,可以使用路由订阅者将这些参数注入到路由中。

三、创建动态团体权限

定义静态权限完全可行,但要创建一组动态权限,我们需要使用权限回调来定义这些权限。

为此,我们首先需要告知“团体”模块我们的动态权限回调类。这是通过在我们定义了多个用于定义权限的类的静态方法的 x.group.permissions.yml 文件中使用 permission_callbacks 标志来完成的。

例如,为了将“编辑团体标题”权限重新定义为一个回调,我们将 custom_group_permissions.group.permissions.yml 文件中的定义修改为以下内容。

  
权限回调:
  - '\Drupal\custom_group_permissions\Access\CustomGroupPermissions::groupPermissions'
  

上面定义的 CustomGroupPermissions 类包含一个名为 groupPermissions() 的方法,“团体”模块在查找权限时会自动调用这个方法。这个方法只需返回一个表示权限的数组即可。

以下是完整的类,它只是复制了我们之前的“编辑团体标题”权限。

  
<?php

namespace Drupal\custom_group_permissions\Access;

/**
 * 为不同类型的团体提供动态权限。
 */
class CustomGroupPermissions {

  /**
   * 返回一组团体类型的权限。
   *
   * @return array
   *   团体权限。
   */
  public function groupPermissions() {
    $perms = [];

    $perms['编辑团体标题'] = [
      '标题' => '编辑团体标题',
      '描述' => '赋予成员编辑团体标题的能力。',
    ];

    return $perms;
  }

}
  

有了这段代码,我们在团体中可用的权限并没有改变,唯一的区别是我们现在是动态生成这些权限。这种技术的真正强大之处在于,它能够围绕站点上的实体和其他数据生成动态权限。

例如,让我们扩展这个权限列表,使其包括每种团体类型附带的所有基本字段。

为了实现这一点,我们需要将 entity_field.manager 服务注入到对象中。Drupal 在实例化类时会检查类定义,如果该类扩展了 \Drupal\Core\DependencyInjection\ContainerInjectionInterface,那么它将调用 create() 方法来生成对象,这样我们就可以将所需的依赖注入到对象中。

一旦完成这些设置,只需使用 groupPermissions() 方法为用户可能在团体定义中更改的每个核心字段返回一个权限。以下是更新后的代码。

  
<?php

namespace Drupal\custom_group_permissions\Access;

use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * 为不同类型的团体提供动态权限。
 */
class CustomGroupPermissions implements ContainerInjectionInterface {

  /**
   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
   */
  protected $entityFieldManager;

  /**
   * {@inheritDoc}
   */
  public static function create(ContainerInterface $container) {
    $instance = new static();
    $instance->setEntityFieldManager($container->get('entity_field.manager'));
    return $instance;
  }

  /**
   * 设置实体字段管理器服务。
   *
   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entityFieldManager
   *   实体字段管理器服务。
   *
   * @return self
   *   当前对象。
   */
  public function setEntityFieldManager(EntityFieldManagerInterface $entityFieldManager): self {
    $this->entityFieldManager = $entityFieldManager;
    return $this;
  }

  /**
   * 返回一组团体类型的权限。
   *
   * @return array
   *   团体权限。
   */
  public function groupPermissions() {
    $perms = [];

    foreach ($this->entityFieldManager->getBaseFieldDefinitions('group') as $field => $definition) {
      if ($definition['只读'] === TRUE) {
        continue;
      }
      $perms['编辑团体 ' . $field] = [
        '标题' => '编辑团体 @字段名',
        '标题参数' => [
          '@字段名' => $definition['标签'],
        ]
      ];
    }

    return $perms;
  }

}
  

这样设置后,我们的团体界面中的权限列表现在已经扩展到包括团体的所有基本字段。当然,我们仍然需要将权限检查代码编写到表单编辑钩子中,但现在我们已经设置好了权限。

使用这个系统,我们可以为内容、分类法、工作流或其他我们可能感兴趣的团体之外的事物创建权限。不过存在一个限制,因为权限回调不接受任何参数,所以我们无法为不同类型的团体定义这些权限。

在处理实体时,考虑使用团体关系插件来完成这项工作可能会更好。

四、为团体添加实体类型权限

虽然权限回调能够创建动态权限,但它们的局限性在于,它们不知道自己所作用的团体类型。这意味着它们适用于所有团体类型,即使你并没有打算在所有类型上使用它们。

解决这个问题的方法是创建一个团体插件,它可以加载到团体中,并用于动态确定该团体的权限。团体插件用于在团体实体和站点中的其他实体之间创建关系,但我们可以利用这个关系构建器的某些部分来创建一个可插拔的权限系统。这可以通过将现有权限应用到团体级别来增强站点上的权限。为此,我们需要创建一个 GroupRelationType 插件,它必须位于自定义模块内的 src/Plugin/Group/Relation 目录中。

以下是一个典型的 GroupRelationType 插件类,它扩展了 \Drupal\group\Plugin\Group\Relation\GroupRelationBase 类。这个基类提供了插件与“团体”模块协同工作所需的所有功能,因此扩展它很有用。

这里有一个 GroupRelationType 插件示例,它将用于控制对附加到该团体的“用户”实体的访问。

  
<?php

namespace Drupal\custom_group_permissions\Plugin\Group\Relation;

use Drupal\group\Plugin\Group\Relation\GroupRelationBase;

/**
 * 为用户实体提供团体关系。
 *
 * @GroupRelationType(
 *   id = "custom_group_permissions",
 *   标签 = @Translation("团体权限"),
 *   描述 = @Translation("为团体添加权限。"),
 *   实体类型 ID = "user",
 *   实体访问 = TRUE
 * )
 */
class PermissionRelation extends GroupRelationBase {

}
  

请注意,这种关系已经通过成员插件实现了,我们只是用自己的一组权限和访问规则来扩展这些权限。也可以创建关系插件而不主动在团体和实体之间建立关系。完全建立关系当然是可行的,但这稍微超出了本文的范围。

这样设置后,你现在可以在自定义团体类型设置中的“团体内容”标签页(路径为“admin/group/types/manage/<group>/content”)中激活它。你应该会在屏幕底部看到以下内容。

点击“安装”将允许该插件与你当前正在查看的团体类型协同工作。

要将此插件用于权限管理,我们必须创建一个服务,“团体”模块会拾取并使用该服务来填充所需的权限。

服务名称必须遵循以下格式:

  
group.relation_handler.$处理程序类型.$团体关系类型 ID
  

这意味着我们必须将服务命名为“group.relation_handler.permission_provider.custom_group_permissions”,因为它是“custom_group_permissions”关系插件的“权限提供者”。上述类的服务定义如下。

  
服务:
  group.relation_handler.permission_provider.custom_group_permissions:
    类: 'Drupal\group\Plugin\Group\RelationHandlerDefault\PermissionProvider'
    参数: ['@entity_type.manager', '@group_relation_type.manager']
    共享: false
  

由于我们要创建一个权限提供者,因此我们创建的服务类必须实现 \Drupal\group\Plugin\Group\RelationHandler\PermissionProviderInterface 接口。如果我们尝试创建一个未实现此接口的权限提供者,“团体”模块将会抛出错误。“团体”模块还附带了一个名为 \Drupal\group\Plugin\Group\RelationHandler\PermissionProviderTrait 的特性,它实现了该接口的所有必需方法,这使我们只需编写实现权限所需的代码即可。

此设置的关键部分是,我们将“group.relation_handler.permission_provider”服务注入到对象中,然后将其设置为类中的父属性。这个父属性是一个 \Drupal\group\Plugin\Group\RelationHandlerDefault\PermissionProvider 对象,用于填补权限提供者的空白。如果没有这一步,你会发现“团体”模块会抛出一些关于缺少父对象的错误。

我们使用此方法创建的自定义权限都围绕着可能对某种实体执行的操作。在我们的例子中,因为我们指定要处理的实体类型是用户实体,“团体”模块会向我们的服务请求针对该实体的查看、更新、删除和创建操作的权限。“团体”模块会遍历实体的可用操作,并依次调用 getPermission() 方法,同时定义此权限的范围(即“自己的”或“任何的”)。权限的目标可以是实体本身,也可以是该实体添加到团体时创建的关系。

在以下示例中,我们为模块设置了单个权限“查看 custom_group_permissions 实体”。在权限设置中,这将对应于用户实体。

  
<?php

namespace Drupal\custom_group_permissions\Plugin\Group\RelationHandler;

use Drupal\group\Plugin\Group\RelationHandler\PermissionProviderInterface;
use Drupal\group\Plugin\Group\RelationHandler\PermissionProviderTrait;

/**
 * 为 custom_group_permissions 关系插件提供团体权限。
 */
class CustomGroupPermissionsProvider implements PermissionProviderInterface {
  use PermissionProviderTrait;

  /**
   * 构造一个新的 GroupMembershipPermissionProvider。
   *
   * @param \Drupal\group\Plugin\Group\RelationHandler\PermissionProviderInterface $parent
   *   父权限提供者。
   */
  public function __construct(PermissionProviderInterface $parent) {
    $this->parent = $parent;
  }

  /**
   * {@inheritdoc}
   */
  public function getPermission($操作, $目标, $范围 = '任何') {
    if ($操作 === '查看' && $目标 === '实体' && $范围 === '任何') {
      return "$操作 $this->插件 ID $目标";
    }
  }

}
  

这样设置后,此权限将出现在团体权限页面上,并作为团体内部的可用权限。

需要注意的是,权限必须与对实体执行的操作相关,这意味着你不能在此处返回任意权限(如“编辑团体标题”),因为它们会被“团体”权限系统忽略。

此权限本身实际上并不会执行任何操作,因此我们现在需要实现一个访问检查。我们首先需要创建一个 access_controller 插件,用于执行权限检查。

  
  group.relation_handler.access_control.custom_group_permissions:
    类: 'Drupal\custom_group_permissions\Plugin\Group\RelationHandler\CustomGroupAccessControl'
    参数: ['@group.relation_handler.access_control']
    共享: false
  

CustomGroupAccessControl 插件类与 CustomGroupPermissionProvider 类类似,但在这种情况下,我们扩展了“团体”模块提供的 \Drupal\group\Plugin\Group\RelationHandler\AccessControlInterface。还有一个方便的 \Drupal\group\Plugin\Group\RelationHandler\AccessControlTrait 特性,我们可以使用它来填补插件的任何空白。“group.relation_handler.access_control” 的父属性会传递给服务,它是一个 \Drupal\group\Plugin\Group\RelationHandlerDefault\AccessControl 对象,我们将其赋值给类中的父属性。

我们在此处创建的 CustomGroupAccessControl 类相当复杂,不过大部分复杂性在于找到与传递给它的实体相关的团体,并检查该团体的权限。我们还确保如果用户是实体的作者(并且他们拥有相应的权限),则他们有权对该实体执行操作。

  
<?php

namespace Drupal\custom_group_permissions\Plugin\Group\RelationHandler;

use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultNeutral;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\group\Plugin\Group\RelationHandler\AccessControlInterface;
use Drupal\group\Plugin\Group\RelationHandler\AccessControlTrait;
use Drupal\user\EntityOwnerInterface;

/**
 * 为 custom_group_permissions 关系插件提供团体权限。
 */
class CustomGroupAccessControl implements AccessControlInterface {
  use AccessControlTrait;

  /**
   * 构造一个新的 CustomGroupAccessControl。
   *
   * @param \Drupal\group\Plugin\Group\RelationHandler\AccessControlInterface $parent
   *   父访问控制处理程序。
   */
  public function __construct(AccessControlInterface $parent) {
    $this->parent = $parent;
  }

  /**
   * {@inheritdoc}
   */
  public function entityAccess(EntityInterface $entity, $操作, AccountInterface $account, $以对象形式返回 = FALSE) {
    // 默认假设我们将返回一个中性的权限检查结果。
    $访问权限 = AccessResultNeutral::中性();

    if ($this->支持操作($操作, '实体') === FALSE) {
      return $访问权限;
    }

    $存储 = $this->entityTypeManager->getStorage('group_relationship');
    $团体关系 = $存储->通过实体加载($entity);
    if (empty($团体关系)) {
      // 如果实体不属于任何团体,我们没有什么可判断的。
      return $访问权限;
    }

    /** @var \Drupal\group\Entity\GroupRelationship $团体关系 */
    foreach ($团体关系 as $团体关系) {
      $团体 = $团体关系->getGroup();
      $访问权限 = AccessResult::允许If($团体->hasPermission("$操作 $this->插件 ID 实体", $账户));

      $所有者访问权限 = $访问权限->orIf(AccessResult::允许If(
        $团体->hasPermission("$操作 $this->插件 ID 实体", $账户)
        && $团体->hasPermission("$操作 自己的 $this->插件 ID 实体", $账户)
        && $实体 instanceof EntityOwnerInterface
        && $实体->getOwnerId() === $账户->id()
      ));

      $访问权限 = $访问权限->orIf($所有者访问权限);

      $访问权限->添加可缓存依赖项($团体关系);
      $访问权限->添加可缓存依赖项($团体);
    }

    return $访问权限;
  }

}
  

这段代码本身并不会执行任何操作,它首先需要在访问检查的场景中被调用。

为此,我们需要使用 hook_entity_access() 钩子的实现来拦截访问检查。在这个钩子中,我们检查与手头实体类型(在我们的例子中是用户)相关的插件,然后使用“group_relation_type.manager”服务加载上述访问控制服务。我们需要以这种方式加载访问控制服务,因为“团体”模块会在对象中填充许多有用的项,如果我们只是自己创建服务,这些项将不会存在。

以下是 hook_entity_access() 钩子的实现。

  
/**
 * 实现 hook_entity_access() 钩子。
 */
function custom_group_permissions_entity_access(\Drupal\Core\Entity\EntityInterface $entity, $操作, \Drupal\Core\Session\AccountInterface $账户) {
  if ($entity->isNew()) {
    return \Drupal\Core\Access\AccessResult::中性();
  }
  /** @var \Drupal\group\Plugin\Group\Relation\GroupRelationTypeManagerInterface $团体关系类型管理器 */
  $团体关系类型管理器 = \Drupal::service('group_relation_type.manager');

  // 查找所有定义对该实体访问权限的团体关系。
  $插件 ID = $团体关系类型管理器->通过实体类型访问获取插件 ID($entity->getEntityTypeId());
  if (empty($插件 ID)) {
    return \Drupal\Core\Access\AccessResult::中性();
  }

  foreach ($插件 ID as $插件) {
    // 尝试加载每个插件服务并检查实体访问权限。
    $服务 = "group.relation_handler.access_control.$插件";
    if (\Drupal::hasService($服务) === TRUE) {
      $插件对象 = $团体关系类型管理器->创建处理程序实例($插件, '访问控制');
      return $插件对象->entityAccess($实体, $操作, $账户);
    }
  }
}
  

现在,当用户访问另一个用户的个人资料页面时,这个访问检查将被触发,并根据 Drupal 和团体本身的权限设置允许或拒绝他们访问。

虽然这种方法可行,但请记住,我们是使用现有的关系插件(即团体成员,也就是用户)来执行我们自己的权限检查,以增强团体现有的权限。这是一个重要的考虑因素,因为如果你想测试不属于团体关系的内容的访问权限,你需要全面实现与关系相关的其他一些代码。一旦这些代码就位,你就可以将这些实体添加到你的团体中,并以与我们这里类似的方式对它们进行权限检查。

五、权限装饰器

最后,值得稍微讨论一下权限装饰器。这是我们获取一个现有的权限检查类并对其进行“装饰”,使其能够理解团体,从而可以对实体执行团体级别的权限检查。这种技术可能相当复杂,但为了完整性,我公司在这里介绍一下,因为它有一个很好的用例,即允许任何权限应用于团体,只要团体和权限之间存在关系。

我们首先要做的是选择一个我们想要装饰的访问检查服务。由于我们处理的是用户实体,我们需要装饰 access_check.entity 服务,因为它提供了我们所需的访问检查。这个服务的定义如下。

  
  access_check.entity:
    类: Drupal\Core\Entity\EntityAccessCheck
    标签:
      - { 名称: access_check, 适用于: _entity_access }
  

为了装饰这个服务,我们需要创建另一个服务定义,并添加一个“decorates”参数,其值为我们想要装饰的服务的名称。我们还传递了额外的参数,以便在这个新服务中使用其他服务。

  
  custom_group_permissions.entity:
    类: 'Drupal\custom_group_permissions\Access\CustomGroupUserPermissions'
    参数: ['@entity_type.manager', '@group_relation_type.manager']
    装饰: access_check.entity
  

这样设置后,我们现在可以创建 CustomGroupUserPermissions 类。这个类的任务与我们之前定义的 CustomGroupAccessControl 类非常相似,即对实体和团体执行一些访问检查。这里的主要区别是,我们需要从路由中加载实体,并使用类似的权限检查来确定用户是否在其所属的任何团体中拥有权限。

以下是 CustomGroupUserPermissions 类的完整源代码。

  
<?php

namespace Drupal\custom_group_permissions\Access;

use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultNeutral;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityAccessCheck;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\group\Plugin\Group\Relation\GroupRelationTypeManagerInterface;
use Drupal\user\EntityOwnerInterface;
use Symfony\Component\Routing\Route;

/**
 * 对团体中的用户实体执行权限检查。
 */
class CustomGroupUserPermissions extends EntityAccessCheck {

  /**
   * 实体类型管理服务。
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * 团体内容启用插件管理器。
   *
   * @var \Drupal\group\Plugin\Group\Relation\GroupRelationTypeManagerInterface
   */
  protected GroupRelationTypeManagerInterface $groupRelationTypeManager;

  /**
   * 构造团体最新修订检查对象。
   *
   * @param \Drupal\content_moderation\ModerationInformationInterface $moderation_information
   *   审核信息服务。
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   实体类型管理服务。
   * @param \Drupal\group\Plugin\Group\Relation\GroupRelationTypeManagerInterface $group_relation_type_manager
   *   团体内容启用插件管理器。
   */
  public function __construct(EntityTypeManagerInterface $entity_type_manager, GroupRelationTypeManagerInterface $group_relation_type_manager) {
    $this->entityTypeManager = $entity_type_manager;
    $this->groupRelationTypeManager = $group_relation_type_manager;
  }

  /**
   * {@inheritDoc}
   */
  public function access(Route $route, RouteMatchInterface $route_match, AccountInterface $account) {
    $访问权限 = parent::access($route, $route_match, $account);
    if (!$访问权限->isAllowed()) {
      // 从路由中加载实体。
      $要求 = $route->getRequirement('_entity_access');
      [$实体类型, $操作] = explode('.', $要求);

      $参数 = $route_match->get参数();
      if ($参数->has($实体类型)) {
        $实体 = $参数->get($实体类型);
        if ($实体 instanceof EntityInterface) {
          // 获取该实体的特定团体访问权限。
          $团体访问权限 = $this->检查团体访问权限($实体, $操作, $账户);
          // 将团体访问权限与上游访问权限合并。
          $访问权限 = $访问权限->orIf($团体访问权限);
        }
      }
    }

    return $访问权限;
  }

  /**
   * 确定对实体的特定团体访问权限。
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   要检查的实体。
   * @param string $operation
   *   要检查访问权限的操作。
   * @param \Drupal\Core\Session\AccountInterface $account
   *   要检查访问权限的用户。
   *
   * @return \Drupal\Core\Access\AccessResultInterface
   *   如果实体属于一个团体,并且用户在其所属的团体中拥有“查看 custom_group_permissions 实体”和“查看自己的 custom_group_permissions 实体”权限,则返回允许访问。
   */
  protected function 检查团体访问权限(ContentEntityInterface $entity, string $operation, AccountInterface $account) {
    // 默认假设我们将返回一个中性的权限检查结果。
    $访问权限 = AccessResultNeutral::中性();

    $存储 = $this->entityTypeManager->getStorage('group_relationship');
    $团体关系 = $存储->通过实体加载($entity);
    if (empty($团体关系)) {
      // 如果实体不属于任何团体,我们没有什么可判断的。
      return $访问权限;
    }

    /** @var \Drupal\group\Entity\GroupRelationship $团体关系 */
    foreach ($团体关系 as $团体关系) {
      $团体 = $团体关系->getGroup();
      $访问权限 = AccessResult::允许If($团体->hasPermission("$操作 $this->插件 ID 实体", $account));

      $所有者访问权限 = $访问权限->orIf(AccessResult::允许If(