[Api] Secure the customer get endpoint

This commit is contained in:
Wojdylak 2024-09-09 13:39:46 +02:00
parent 7efc67e18b
commit a30537ec7f
No known key found for this signature in database
GPG key ID: 7509E560A6821ABE
4 changed files with 275 additions and 2 deletions

View file

@ -0,0 +1,92 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Doctrine\ORM\QueryExtension\Shop\Customer;
use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Doctrine\Orm\Extension\QueryItemExtensionInterface;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\Operation;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use Sylius\Bundle\ApiBundle\Context\UserContextInterface;
use Sylius\Bundle\ApiBundle\SectionResolver\ShopApiSection;
use Sylius\Bundle\CoreBundle\SectionResolver\SectionProviderInterface;
use Sylius\Component\Core\Model\CustomerInterface;
final readonly class ShopUserBasedExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
{
public function __construct(
private SectionProviderInterface $sectionProvider,
private UserContextInterface $userContext,
) {
}
/**
* @param array<array-key, mixed> $context
*/
public function applyToCollection(
QueryBuilder $queryBuilder,
QueryNameGeneratorInterface $queryNameGenerator,
string $resourceClass,
?Operation $operation = null,
array $context = [],
): void {
$this->filterOutOrders($queryBuilder, $queryNameGenerator, $resourceClass);
}
/**
* @param array<array-key, mixed> $identifiers
* @param array<array-key, mixed> $context
*/
public function applyToItem(
QueryBuilder $queryBuilder,
QueryNameGeneratorInterface $queryNameGenerator,
string $resourceClass,
array $identifiers,
?Operation $operation = null,
array $context = [],
): void {
$this->filterOutOrders($queryBuilder, $queryNameGenerator, $resourceClass);
}
private function filterOutOrders(
QueryBuilder $queryBuilder,
QueryNameGeneratorInterface $queryNameGenerator,
string $resourceClass,
): void {
if (!is_a($resourceClass, CustomerInterface::class, true)) {
return;
}
if (!$this->sectionProvider->getSection() instanceof ShopApiSection) {
return;
}
$user = $this->userContext->getUser();
$rootAlias = $queryBuilder->getRootAliases()[0];
$userJoinAlias = $queryNameGenerator->generateJoinAlias('user');
$userParameterName = $queryNameGenerator->generateParameterName('user');
$queryBuilder
->innerJoin(
sprintf('%s.user', $rootAlias),
$userJoinAlias,
Join::WITH,
$queryBuilder->expr()->eq(sprintf('%s.id', $userJoinAlias), sprintf(':%s', $userParameterName))
)
->setParameter($userParameterName, $user)
;
}
}

View file

@ -198,5 +198,15 @@
<tag name="api_platform.doctrine.orm.query_extension.collection" />
<tag name="api_platform.doctrine.orm.query_extension.item" />
</service>
<service
id="sylius_api.doctrine.orm.query_extension.shop.customer.shop_user_based"
class="Sylius\Bundle\ApiBundle\Doctrine\ORM\QueryExtension\Shop\Customer\ShopUserBasedExtension"
>
<argument type="service" id="sylius.section_resolver.uri_based_section_resolver" />
<argument type="service" id="Sylius\Bundle\ApiBundle\Context\UserContextInterface" />
<tag name="api_platform.doctrine.orm.query_extension.collection" />
<tag name="api_platform.doctrine.orm.query_extension.item" />
</service>
</services>
</container>

View file

@ -0,0 +1,140 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace spec\Sylius\Bundle\ApiBundle\Doctrine\ORM\QueryExtension\Shop\Customer;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\Get;
use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\QueryBuilder;
use PhpSpec\ObjectBehavior;
use Sylius\Bundle\ApiBundle\Context\UserContextInterface;
use Sylius\Bundle\ApiBundle\SectionResolver\AdminApiSection;
use Sylius\Bundle\ApiBundle\SectionResolver\ShopApiSection;
use Sylius\Bundle\CoreBundle\SectionResolver\SectionProviderInterface;
use Sylius\Component\Core\Model\AdminUserInterface;
use Sylius\Component\Core\Model\CustomerInterface;
use Sylius\Component\Core\Model\ShopUserInterface;
use Sylius\Resource\Model\ResourceInterface;
final class ShopUserBasedExtensionSpec extends ObjectBehavior
{
function let(
SectionProviderInterface $sectionProvider,
UserContextInterface $userContext,
): void {
$this->beConstructedWith($sectionProvider, $userContext);
}
function it_does_not_apply_conditions_to_collection_for_unsupported_resource(
UserContextInterface $userContext,
QueryBuilder $queryBuilder,
QueryNameGeneratorInterface $queryNameGenerator,
): void {
$userContext->getUser()->shouldNotBeCalled();
$queryBuilder->getRootAliases()->shouldNotBeCalled();
$this->applyToCollection($queryBuilder, $queryNameGenerator, ResourceInterface::class, new Get());
}
function it_does_not_apply_conditions_to_collection_for_admin_api_section(
SectionProviderInterface $sectionProvider,
UserContextInterface $userContext,
QueryBuilder $queryBuilder,
QueryNameGeneratorInterface $queryNameGenerator,
AdminApiSection $section,
): void {
$sectionProvider->getSection()->willReturn($section);
$userContext->getUser()->shouldNotBeCalled();
$queryBuilder->getRootAliases()->shouldNotBeCalled();
$this->applyToCollection($queryBuilder, $queryNameGenerator, CustomerInterface::class, new Get());
}
function it_applies_conditions_to_collection(
SectionProviderInterface $sectionProvider,
UserContextInterface $userContext,
QueryBuilder $queryBuilder,
QueryNameGeneratorInterface $queryNameGenerator,
ShopApiSection $section,
ShopUserInterface $user,
Expr $expr,
Expr\Func $exprEq,
): void {
$sectionProvider->getSection()->willReturn($section);
$userContext->getUser()->willReturn($user);
$queryBuilder->getRootAliases()->willReturn(['o']);
$queryNameGenerator->generateJoinAlias('user')->willReturn('user');
$queryNameGenerator->generateParameterName('user')->willReturn('user');
$queryBuilder->expr()->willReturn($expr);
$expr->eq('user.id', ':user')->willReturn($exprEq);
$queryBuilder->innerJoin('o.user', 'user', 'WITH', $exprEq)->shouldBeCalled()->willReturn($queryBuilder->getWrappedObject());
$queryBuilder->setParameter('user', $user)->shouldBeCalled()->willReturn($queryBuilder->getWrappedObject());
$this->applyToCollection($queryBuilder, $queryNameGenerator, CustomerInterface::class, new Get());
}
function it_does_not_apply_conditions_to_item_for_unsupported_resource(
UserContextInterface $userContext,
QueryBuilder $queryBuilder,
QueryNameGeneratorInterface $queryNameGenerator,
): void {
$userContext->getUser()->shouldNotBeCalled();
$queryBuilder->getRootAliases()->shouldNotBeCalled();
$this->applyToItem($queryBuilder, $queryNameGenerator, ResourceInterface::class, [], new Get());
}
function it_does_not_apply_conditions_to_item_for_admin_api_section(
SectionProviderInterface $sectionProvider,
UserContextInterface $userContext,
QueryBuilder $queryBuilder,
QueryNameGeneratorInterface $queryNameGenerator,
AdminApiSection $section,
): void {
$sectionProvider->getSection()->willReturn($section);
$userContext->getUser()->shouldNotBeCalled();
$queryBuilder->getRootAliases()->shouldNotBeCalled();
$this->applyToItem($queryBuilder, $queryNameGenerator, CustomerInterface::class, [], new Get());
}
function it_applies_conditions_to_item(
SectionProviderInterface $sectionProvider,
UserContextInterface $userContext,
QueryBuilder $queryBuilder,
QueryNameGeneratorInterface $queryNameGenerator,
ShopApiSection $section,
ShopUserInterface $user,
CustomerInterface $customer,
Expr $expr,
Expr\Func $exprEq,
): void {
$user->getCustomer()->willReturn($customer);
$sectionProvider->getSection()->willReturn($section);
$userContext->getUser()->willReturn($user);
$queryBuilder->getRootAliases()->willReturn(['o']);
$queryNameGenerator->generateJoinAlias('user')->willReturn('user');
$queryNameGenerator->generateParameterName('user')->willReturn('user');
$queryBuilder->expr()->willReturn($expr);
$expr->eq('user.id', ':user')->willReturn($exprEq);
$queryBuilder->innerJoin('o.user', 'user', 'WITH', $exprEq)->shouldBeCalled()->willReturn($queryBuilder->getWrappedObject());
$queryBuilder->setParameter('user', $user)->shouldBeCalled()->willReturn($queryBuilder->getWrappedObject());
$this->applyToItem($queryBuilder, $queryNameGenerator, CustomerInterface::class, [], new Get());
}
}

View file

@ -31,7 +31,37 @@ final class CustomersTest extends JsonApiTestCase
}
/** @test */
public function it_gets_customer(): void
public function it_gets_customer_as_logged_user(): void
{
$fixtures = $this->loadFixturesFromFiles(['authentication/shop_user.yaml']);
/** @var CustomerInterface $customer */
$customer = $fixtures['customer_oliver'];
$this->requestGet(
uri: '/api/v2/shop/customers/' . $customer->getId(),
headers: $this->headerBuilder()->withShopUserAuthorization($customer->getEmailCanonical())->build(),
);
$this->assertResponse($this->client->getResponse(), 'shop/customer/get_customer_response');
}
/** @test */
public function it_gets_customer_as_another_logged_user(): void
{
$fixtures = $this->loadFixturesFromFiles(['authentication/shop_user.yaml']);
/** @var CustomerInterface $customer */
$customer = $fixtures['customer_oliver'];
$this->requestGet(
uri: '/api/v2/shop/customers/' . $customer->getId(),
headers: $this->headerBuilder()->withShopUserAuthorization('dave@doe.com')->build(),
);
$this->assertResponseCode($this->client->getResponse(), Response::HTTP_NOT_FOUND);
}
/** @test */
public function it_gets_customer_as_guest(): void
{
$fixtures = $this->loadFixturesFromFiles(['authentication/shop_user.yaml']);
/** @var CustomerInterface $customer */
@ -39,7 +69,7 @@ final class CustomersTest extends JsonApiTestCase
$this->requestGet('/api/v2/shop/customers/' . $customer->getId());
$this->assertResponse($this->client->getResponse(), 'shop/customer/get_customer_response');
$this->assertResponseCode($this->client->getResponse(), Response::HTTP_NOT_FOUND);
}
/** @test */
@ -95,6 +125,7 @@ final class CustomersTest extends JsonApiTestCase
'birthday' => '2023-10-24T11:00:00.000Z',
'subscribedToNewsletter' => true,
],
headers: $this->headerBuilder()->withShopUserAuthorization($customer->getEmailCanonical())->build(),
);
$this->assertResponse($this->client->getResponse(), 'shop/customer/put_customer_response');