不使用自定义子资源路径时关系实体字段上的安全选民
Security voter on relational entity field when not using custom subresource path
我已经开始在我们的应用程序中做一些更高级的安全性事情,公司可以在其中为每个模块使用可自定义的 CRUD 创建自己的用户角色,这意味着您可以在您设置的位置创建自定义角色“用户只读”读取”为“2”,为用户模块创建、更新、删除为 0。团队模块也是如此。
- 0表示他根本无权访问
- 1表示可以访问所有公司下的数据,
- 2 表示只能访问与他相关的东西(如果他是所有者
另一个用户),
这会导致以下行为:当用户通过获取请求请求团队时,return团队中的用户是团队中的用户,但是,因为用户角色配置为$capabilities["users"]["read"] = 2
,那么team.users
应该只包含他,没有其他团队成员,因为用户看不到除了他自己和他创建的用户之外的用户。
到目前为止,我已经设法通过实施 QueryCollectionExtensionInterface
的学说扩展来限制收集-获取操作,并过滤掉 return 给用户的结果:
- 当我查询具有
$capabilities["teams"]["read"] = 2
的角色时,集合 return 仅用户所属的团队或他创建的团队。
- 当我查询具有
$capabilities["teams"]["read"] = 1
角色的用户时,它 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'
这样您就可以保持扩展的优势。
我已经开始在我们的应用程序中做一些更高级的安全性事情,公司可以在其中为每个模块使用可自定义的 CRUD 创建自己的用户角色,这意味着您可以在您设置的位置创建自定义角色“用户只读”读取”为“2”,为用户模块创建、更新、删除为 0。团队模块也是如此。
- 0表示他根本无权访问
- 1表示可以访问所有公司下的数据,
- 2 表示只能访问与他相关的东西(如果他是所有者 另一个用户),
这会导致以下行为:当用户通过获取请求请求团队时,return团队中的用户是团队中的用户,但是,因为用户角色配置为$capabilities["users"]["read"] = 2
,那么team.users
应该只包含他,没有其他团队成员,因为用户看不到除了他自己和他创建的用户之外的用户。
到目前为止,我已经设法通过实施 QueryCollectionExtensionInterface
的学说扩展来限制收集-获取操作,并过滤掉 return 给用户的结果:
- 当我查询具有
$capabilities["teams"]["read"] = 2
的角色时,集合 return 仅用户所属的团队或他创建的团队。 - 当我查询具有
$capabilities["teams"]["read"] = 1
角色的用户时,它 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'
这样您就可以保持扩展的优势。