不使用自定义子资源路径时关系实体字段的安全投票者 [英] Security voter on relational entity field when not using custom subresource path

查看:15
本文介绍了不使用自定义子资源路径时关系实体字段的安全投票者的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I have started doing some more advanced security things in our application, where companies can create their own user roles with customizable CRUD for every module, which means you can create a custom role "Users read only" where you set "read" to "2" and create, update, delete to 0 for the user module. And the same for the teams module.

  • 0 表示他根本没有访问权限.
  • 1 表示可以访问公司下的所有数据,
  • 2 表示只能访问与他相关的东西(如果他是所有者另一个用户),

这应该导致这样的行为,当用户通过 get 请求请求一个团队时,它返回带有团队中用户的团队,但是,因为用户角色配置了 $capabilities["users"]["read"] = 2,那么team.users应该只包含他,没有其他团队成员,因为用户除了他自己和他的用户之外看不到其他用户已创建.

Which should result in the behavior that, when user requests a team over a get request, it returns the team with the users that are in the team, BUT, since the user role is configured with $capabilities["users"]["read"] = 2, then team.users should contain only him, without the other team members, because user cannot see users except himself and users that he created.

到目前为止,我已经设法通过实现 QueryCollectionExtensionInterface 的学说扩展来限制 collection-get 操作,并过滤掉返回给用户的结果:

So far I have managed to limit collection-get operations with a doctrine extension that implements QueryCollectionExtensionInterface and filters out what results to return to the user:

  • 当我使用具有 $capabilities[teams"][read"] = 2 的角色进行查询时,该集合仅返回用户所属的团队,或他所属的团队已创建.
  • 当我查询具有 $capabilities[teams"][read"] = 1 角色的用户时,它会返回公司内部的所有团队.哪个是正确的.
  • when I query with a role that has $capabilities["teams"]["read"] = 2 then the collection returns only teams that user is part of, or teams that he created.
  • when I query for users with role that has $capabilities["teams"]["read"] = 1 then it returns all teams inside the company. Which is correct.

当我查询单个团队时出现问题.为了确保项目操作的安全性,我使用了 Voters,它在获取/更新/插入/...一个新实体到数据库之前检查用户能力,这工作正常.

The problem comes when I query a single team. For security on item operations I use Voters, which checks the user capabilities before getting/updating/inserting/... a new entity to the DB, which works fine.

所以问题是,当团队返回时,来自多对多用户<->团队关系的用户列表包含属于团队的所有用户.我需要以某种方式过滤掉它以匹配我的角色能力.因此,在这种情况下,如果用户具有 $capabilities[users"][read"] = 2,则 team.users 应仅包含用户请求,因为他有权列出他所在的团队,但他无权查看除自己之外的其他用户.

So the problem is, that when the team is returned, the user list from the manytomany user<->team relation, contains all the users that are part of the team. I need to somehow filter out this to match my role capabilities. So in this case if the user has $capabilities["users"]["read"] = 2, then the team.users should contain only the user making the request, because he has access to list the teams he is in, but he has no permission to view other users than himself.

所以我的问题是,如何在项目操作和集合操作的关系字段上添加安全投票者.

So my question is, how can add a security voter on relational fields for item-operations and collection-operations.

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

A rough visual representation of what I want to achieve

    /**
     * @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 不是一个好的解决方案.

I don't think Normalizer is a good solution from a performance perspective, considering that the DB query was already made.

推荐答案

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

If I understand well, in the end the only problem is that when you make a request GET /api/teams/{id}, the property $users contains all users belonging to the team, but given user's permissions, you just want to display a subset.

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

Indeed Doctrine Extensions are not enough because they only limits the number of entities of the targeted entity, i.e Team in your case.

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

But it seems that Doctrine Filters cover this use case; they allow to add extra SQL clauses to your queries, even when fetching associated entities. But I never used them myself so I can't be 100% sure. Seems to be a very low level tool.

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

Otherwise, I deal with a similar use case on my project, but yet I'm not sure it fit all your needs:

  • 添加一个额外的 $members 数组属性,没有任何 @ORM 注释,
  • 从序列化中排除 $users 关联属性,将其替换为 $members
  • 装饰Team实体的数据提供者,
  • 使修饰的数据提供者用一组受限的用户填充新属性.
  • Adding an extra $members array property without any @ORM annotation,
  • Excluding the $users association property from serialization, replacing it by $members,
  • Decorating the data provider of the Team entity,
  • Making the decorated data provider fill the new property with a restricted set of users.
// 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 数据提供者.我只是忘记了这些服务使用别名.你需要配置一下:

The constructor of the TeamDataProvider use concrete classes instead of interfaces because it is meant to decorate precisely ORM data providers. I just forgot that those services use aliases. You need to configure a bit:

#  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'

这样您就可以保持扩展程序的优势.

This way you keep advantages of your extensions.

这篇关于不使用自定义子资源路径时关系实体字段的安全投票者的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆