不使用自定义子资源路径时关系实体字段上的安全选民

Security voter on relational entity field when not using custom subresource path

我已经开始在我们的应用程序中做一些更高级的安全性事情,公司可以在其中为每个模块使用可自定义的 CRUD 创建自己的用户角色,这意味着您可以在您设置的位置创建自定义角色“用户只读”读取”为“2”,为用户模块创建、更新、删除为 0。团队模块也是如此。

这会导致以下行为:当用户通过获取请求请求团队时,return团队中的用户是团队中的用户,但是,因为用户角色配置为$capabilities["users"]["read"] = 2,那么team.users应该只包含他,没有其他团队成员,因为用户看不到除了他自己和他创建的用户之外的用户。

到目前为止,我已经设法通过实施 QueryCollectionExtensionInterface 的学说扩展来限制收集-获取操作,并过滤掉 return 给用户的结果:

当我查询单个团队时,问题就来了。为了项目操作的安全性,我使用 Voters,它在 getting/updating/inserting/... 一个新实体到数据库之前检查用户能力,它工作正常。

所以问题是,当团队被 returned 时,来自 manytomany user<->team 关​​系的用户列表包含属于团队的所有用户。我需要以某种方式过滤掉它以匹配我的角色能力。所以在这种情况下,如果用户有 $capabilities["users"]["read"] = 2,那么 team.users 应该只包含发出请求的用户,因为他有权列出他所在的团队,但他无权查看其他团队用户比他自己

所以我的问题是,如何在项目操作和集合操作的关系字段上添加安全选民。

我想要实现的目标的粗略视觉表示

    /**
     * @ORM\ManyToMany(targetEntity="User", mappedBy="teams")
     * @Groups({"team.read","form.read"})
     * @Security({itemOperations={
 *         "get"={
 *              "access_control"="is_granted('user.view', object)",
 *              "access_control_message"="Access denied."
 *          },
 *         "put"={
 *              "access_control"="is_granted('user.update', object)",
 *              "access_control_message"="Access denied."
 *          },
 *         "delete"={
 *              "access_control"="is_granted('user.delete', object)",
 *              "access_control_message"="Access denied."
 *          },
 *      },
 *      collectionOperations={
 *          "get"={
 *              "access_control"="is_granted('user.list', object)",
 *              "access_control_message"="Access denied."
 *          },
 *          "post"={
 *              "access_control"="is_granted('user.create', object)",
 *              "access_control_message"="Access denied."
 *          },
 *      }})
     */
    private $users;

考虑到已经进行了数据库查询,从性能的角度来看,我认为 Normalizer 不是一个好的解决方案。

如果我理解得很好,最后唯一的问题是当你发出请求GET /api/teams/{id}时,属性 $users包含属于团队的所有用户,但是给出用户的权限,你只想显示一个子集。

确实,Doctrine Extensions 是不够的,因为它们只限制了目标实体的实体数量,即在你的情况下 Team

但似乎 Doctrine Filters 涵盖了这个用例;它们允许向您的查询添加额外的 SQL 子句,即使在获取关联实体时也是如此。但我自己从未使用过它们,所以我不能 100% 确定。似乎是一个非常低级的工具。

否则,我在我的项目中处理了类似的用例,但我不确定它是否满足您的所有需求:

  • 添加一个额外的 $members 数组 属性 没有任何 @ORM 注释,
  • 从序列化中排除 $users 关联 属性,将其替换为 $members
  • 装饰 Team 实体的数据提供者,
  • 使经过修饰的数据提供程序用一组受限的用户填充新的 属性。
// src/Entity/Team.php
/**
 * @ApiResource(
 *     ...
 * )
 * @ORM\Entity(repositoryClass=TeamRepository::class)
 */
class Team
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @var User[]
     * @ORM\ManyToMany(targetEntity=User::class)  //This property is persisted but not serialized
     */
    private $users;

    /**
     * @var User[]  //This property is not persisted but serialized
     * @Groups({read:team, ...})
     */
    private $members = [];
// src/DataProvider/TeamDataProvider.php
class TeamDataProvider implements CollectionDataProviderInterface, ItemDataProviderInterface, RestrictedDataProviderInterface
{
    /** @var ItemDataProvider */
    private $itemDataProvider;

    /** @var CollectionDataProvider*/
    private $collectionDataProvider;

    /** @var Security  */
    private $security;

    public function __construct(ItemDataProvider $itemDataProvider, 
                                CollectionDataProvider $collectionDataProvider,
                                Security $security)
    {
        $this->itemDataProvider = $itemDataProvider;
        $this->collectionDataProvider = $collectionDataProvider;
        $this->security = $security;
    }

    public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
    {
        return $resourceClass === Team::class;
    }

    public function getCollection(string $resourceClass, string $operationName = null)
    {
        /** @var Team[] $manyTeams */
        $manyTeams = $this->collectionDataProvider->getCollection($resourceClass, $operationName);
        foreach ($manyTeams as $team) {
            $this->fillMembersDependingUserPermissions($team);
        }
        return $manyTeams;
    }

    public function getItem(string $resourceClass, $id, string $operationName = null, array $context = [])
    {
        /** @var Team|null $team */
        $team = $this->itemDataProvider->getItem($resourceClass, ['id' => $id], $operationName, $context);
        if ($team !== null) {
            $this->fillMembersDependingUserPermissions($team);
        }
        return $team;
    }

    private function fillMembersDependingUserPermissions(Team $team): void
    {
        $currentUser = $this->security->getUser();
        if ($currentUser->getCapabilities()['users']['read'] === 2) {
            $team->setMembers([$currentUser]);
        } elseif ($currentUser->getCapabilities()['users']['read'] === 1) {
            $members = $team->getUsers()->getValues();
            $team->setMembers($members);  //Current user is already within the collection
        }
    }
}

回复后编辑

TeamDataProvider 的构造函数使用具体 类 而不是接口,因为它旨在 精确地 ORM 数据提供者。我只是忘记了那些服务使用别名。你需要配置一下:

#  config/services.yaml

App\DataProvider\TeamDataProvider:
    arguments:
        $itemDataProvider: '@api_platform.doctrine.orm.default.item_data_provider'
        $collectionDataProvider: '@api_platform.doctrine.orm.default.collection_data_provider'

这样您就可以保持扩展的优势。