ApiPlatform - 基于 apiplatform 过滤器实现授权

时间:2021-01-04 11:18:56

标签: symfony filter authorization api-platform.com symfony4-voter

我正在使用 ApiPlatformSymfony5

我在 User 实体上放置了一个过滤器,以按名为 $expose 的类的布尔值对它们进行排序

用例:

  • 对于 /users?expose=true 路由,ROLE_USER 可以获取每个 user 的列表,过滤器 $expose 设置为 true
  • 对于 /users/ 路线,ROLE_ADMIN 无论如何都可以获取每个 user 的列表

这是我的 User 课:


/**
 * @ApiResource(
 *     attributes={
 *          "normalization_context"={"groups"={"user:read", "user:list"}},
 *          "order"={"somefield.value": "ASC"}
 *     },
 *     collectionOperations={
 *          "get"={
 *              "mehtod"="GET",
 *              "security"="is_granted('LIST', object)",
 *              "normalization_context"={"groups"={"user:list"}},
 *          }
 *     }
 * )
 * @ApiFilter(ExistsFilter::class, properties={"expose"})
 * @ApiFilter(SearchFilter::class, properties={
 *     "somefield.name": "exact"
 * })
 * @ORM\Entity(repositoryClass=UserRepository::class)
 */

我通过 UserVoter 实施我的授权规则:

protected function supports($attribute, $subject): bool
    {
        return parent::supports($attribute, $subject) &&
            ($subject instanceof User ||
                $this->arrayOf($subject, User::class)  ||
                (is_a($subject, Paginator::class) &&
                    $this->arrayOf($subject->getQuery()->getResult(), User::class))
            );
    }

    protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
    {
        /** @var User $user */
        $user = $token->getUser();

        if (!$user instanceof User) {
            return false;
        }

        if ($this->accessDecisionManager->decide($token, [GenericRoles::ROLE_ADMIN])) {
            return true;
        }

        switch ($attribute) {
            case Actions::LIST:
                break;
        }
        return false;
    }

为了恢复 User 的列表,我恢复了通过 LIST 属性传递的分页器对象,并确保请求结果中的对象属于 User 类型。 这部分已经过测试并且可以正常工作。

现在我的问题来自这样一个事实,即这两条路线对我的 voter 来说本质上是相同的,因此我通过它实施的授权规则对它们都适用。

我想做的是告诉我的选民这两个请求是不同的(我认为我可以在恢复 Paginator 对象时这样做,但似乎不可能)所以我可以分别对待它们在同一个开关盒中。

到目前为止我还没有找到实现它的方法

有没有办法实现这种规则?

或者有其他方式实现这种授权吗?

谢谢!

2 个答案:

答案 0 :(得分:0)

如果您可以使用相同的请求 /users/ 处理普通用户和管理员用户但得到不同的结果, this docs page 描述了一种使 GET 收集操作的结果取决于登录的用户的方法。我针对您的问题对其进行了修改:

<?php
// api/src/Doctrine/CurrentUserExtension.php

namespace App\Doctrine;

use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use App\Entity\Offer;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Security\Core\Security;

final class CurrentUserExtension implements QueryCollectionExtensionInterface
{
    private $security;

    public function __construct(Security $security)
    {
        $this->security = $security;
    }

    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null): void
    {
        if (User::class !== $resourceClass || $this->security->isGranted('ROLE_ADMIN')) {
            return;
        }

        $rootAlias = $queryBuilder->getRootAliases()[0];
        $queryBuilder->andWhere("$rootAlias.expose = true");
    }
}

顺便说一句,任何没有 ROLE_ADMIN 的用户都会得到过滤结果,不需要 ROLE_USER。

答案 1 :(得分:0)

如果您选择坚持要求具有 ROLE_USER 的用户使用 /users?expose=true 的用例,您可以创建一个自定义的 CollectionDataProvider 来抛出 FilterValidationException:

<?php

namespace App\DataProvider;

use Symfony\Component\Security\Core\Security;
use ApiPlatform\Core\DataProvider\ContextAwareCollectionDataProviderInterface;
use ApiPlatform\Core\DataProvider\CollectionDataProviderInterface;
use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface;
use ApiPlatform\Core\Exception\FilterValidationException;
use App\Entity\User;

class UserCollectionDataProvider implements ContextAwareCollectionDataProviderInterface, RestrictedDataProviderInterface
{
    /** @var CollectionDataProviderInterface */
    private $dataProvider;

    private $security;

    /**
     * @param CollectionDataProviderInterface $dataProvider The built-in orm CollectionDataProvider of API Platform
     */
    public function __construct(CollectionDataProviderInterface $dataProvider, Security $security)
    {
        $this->dataProvider = $dataProvider;
        $this->security = $security;
    }

    /**
     * {@inheritdoc}
     */
    public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
    {
        return User::class === $resourceClass;
    }

    /** throws FilterValidationException */
    private function validateFilters($context)
    {
    if ($this->security->isGranted('ROLE_ADMIN')) {
            // Allow any filters, including no filters
            return;
        }
        if (!$this->security->isGranted('ROLE_USER')) {
            throw new \LogicException('No use case has been defined for this situation');
        }

        $errorList = [];
        if (!isset($context["filters"]["expose"]) ||
            $context["filters"]["expose"] !== "true" && $context["filters"]["expose"] !== '1'
        ) {
            $errorList[] = 'expose=true filter is required.'
            throw new FilterValidationException($errorList);
        }
    }

    /**
     * {@inheritdoc}
     * @throws FilterValidationException;
     */
    public function getCollection(string $resourceClass, string $operationName = null, array $context = []): array
    {
        $this->validateFilters($context);

        return $this->dataProvider->getCollection($resourceClass, $operationName, $context);
    }

您确实需要在 api/config/services.yaml 中添加以下内容:

'App\DataProvider\UserCollectionDataProvider':
    arguments:
        $dataProvider: '@api_platform.doctrine.orm.default.collection_data_provider'

顺便说一句,要通过布尔值进行过滤,通常使用 BooleanFilter:

 * @ApiFilter(BooleanFilter::class, properties={"expose"})

这是相关的,因为具有 ROLE_ADMIN 的用户可能会尝试通过 Exposure=false 进行过滤。顺便说一句,如果 $expose 可以为空,您需要测试将 $expose 设置为 null 的用户会发生什么

警告:请注意,如果属性 $expose 不再映射,或者属性 $expose 的名称已更改但在 UserCollectionDataProvider 中,则您的安全性将静默失败,允许所有用户访问所有用户实体它不是或过滤器规范不是!