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

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

问题描述

我已经开始在我们的应用程序中做一些更高级的安全性工作,公司可以为每个模块创建带有可定制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表示只能访问与他有关的内容(如果他是所有者)另一个用户)

这将导致以下行为:当用户通过获取请求请求团队时,由于用户角色配置了 $ capabilities [,因此它将返回团队中的用户BUT.; 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 并删除返回给用户的结果的学说扩展来限制集合获取操作:

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.

仅是理论上的扩展是不够的,因为它们仅限制目标实体的实体数量,即您的案例中的 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.

但是,似乎教义过滤器涵盖此用例;它们允许在查询中添加额外的SQL子句,即使在获取关联的实体时也是如此.但是我从来没有亲自使用过它们,所以我不能百分百确定.似乎是一个非常低级的工具.

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:

  • 添加一个没有任何 @ORM 批注的额外的 $ members 数组属性,
  • 从序列化中排除 $ 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天全站免登陆