Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions Api/Extension/AdminSaleUnitScopeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

declare(strict_types=1);

namespace OrderComment\Api\Extension;

use ApiPlatform\Metadata\HttpOperation;
use ApiPlatform\Metadata\Operation;
use OrderComment\Api\Resource\OrderComment;
use OrderComment\Model\Map\OrderCommentTableMap;
use Propel\Runtime\ActiveQuery\Criteria;
use Propel\Runtime\ActiveQuery\ModelCriteria;
use Thelia\Api\Bridge\Propel\Extension\QueryCollectionExtensionInterface;
use Thelia\Api\Bridge\Propel\Extension\QueryItemExtensionInterface;
use Thelia\Domain\SaleUnit\Access\AdminSaleUnitScope;

/**
* Narrows the admin order comments of an administrator restricted to some sale
* units to the comments of the orders placed on those units. Collections lose the
* other rows and an item out of reach answers 404, as for the orders themselves.
*
* An administrator attached to no unit, and a superadministrator, see everything.
* A comment on a national order (no unit) belongs to none of a restricted
* administrator's units, so it is left out.
*/
final readonly class AdminSaleUnitScopeExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
{
public function __construct(private AdminSaleUnitScope $scope)
{
}

public function applyToCollection(ModelCriteria $query, string $resourceClass, ?Operation $operation = null, array $context = []): void
{
$this->restrict($query, $resourceClass, $operation);
}

public function applyToItem(ModelCriteria $query, string $resourceClass, ?Operation $operation = null, array $context = []): void
{
$this->restrict($query, $resourceClass, $operation);
}

private function restrict(ModelCriteria $query, string $resourceClass, ?Operation $operation): void
{
if (OrderComment::class !== $resourceClass) {
return;
}

if (!$operation instanceof HttpOperation || !str_starts_with($operation->getUriTemplate() ?? '', '/admin')) {
return;
}

$allowed = $this->scope->allowedSaleUnitIds();

if (null === $allowed) {
return;
}

// The ids are integers read from the database, never from the request.
$query->addAnd(OrderCommentTableMap::COL_ORDER_ID, \sprintf(
'%s IN (SELECT `order`.`id` FROM `order` WHERE `order`.`sale_unit_id` IN (%s))',
OrderCommentTableMap::COL_ORDER_ID,
implode(',', array_map('intval', $allowed)) ?: 'NULL',
), Criteria::CUSTOM);
}
}
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Changelog

## 3.1.1

### Fixed

- The admin `order-comments` API collection is restricted to the comments of the orders placed on the sale units of an administrator restricted to some units (a comment on a national order is left out). An administrator attached to no unit and a superadministrator are unchanged.
- The `order-comments` admin resource now declares its permission (the order one) in `thelia.api.admin_resources`; without it the admin API refuses every non-super administrator.

### Changed

- The admin permission of the resource is added to `thelia.api.admin_resources` by a compiler pass, so it never erases what another module declared.

- Requires `thelia/core` 3.3.0 or later (sale unit scope).
4 changes: 2 additions & 2 deletions Config/module.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
<descriptive locale="fr_FR">
<title>Permet au client d'ajouter un commentaire à sa commande</title>
</descriptive>
<version>3.1.0</version>
<version>3.1.1</version>
<author>
<name>Vincent Lopes-Vicente</name>
<email>vlopes@openstudio.fr</email>
</author>
<type>classic</type>
<thelia>3.0.0</thelia>
<thelia>3.3.0</thelia>
<stability>prod</stability>
</module>
33 changes: 33 additions & 0 deletions DependencyInjection/AdminApiResourcesPass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace OrderComment\DependencyInjection;

use OrderComment\Api\Resource\OrderComment as OrderCommentResource;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Thelia\Core\Security\Resource\AdminResources;

/**
* Gives the order comments the order permission: an order comment is shown on the order.
* Without an entry in `thelia.api.admin_resources`, the admin API refuses the endpoint to
* every administrator but a superadministrator.
*
* Merged rather than set: the parameter is one array shared by every module publishing
* admin resources, and setting it would erase the others.
*/
final readonly class AdminApiResourcesPass implements CompilerPassInterface
{
private const PARAMETER = 'thelia.api.admin_resources';

public function process(ContainerBuilder $container): void
{
$declared = $container->hasParameter(self::PARAMETER) ? (array) $container->getParameter(self::PARAMETER) : [];

$container->setParameter(self::PARAMETER, [
...$declared,
OrderCommentResource::class => AdminResources::ORDER,
]);
}
}
8 changes: 7 additions & 1 deletion OrderComment.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

use OrderComment\Model\OrderCommentQuery;
use Propel\Runtime\Connection\ConnectionInterface;
use OrderComment\DependencyInjection\AdminApiResourcesPass;
use Symfony\Component\DependencyInjection\Loader\Configurator\ServicesConfigurator;
use Thelia\Core\Install\Database;
use Thelia\Module\BaseModule;
Expand All @@ -39,10 +40,15 @@ public function postActivation(?ConnectionInterface $con = null): void
}
}

public static function getCompilers(): array
{
return [new AdminApiResourcesPass()];
}

public static function configureServices(ServicesConfigurator $servicesConfigurator): void
{
$servicesConfigurator->load(self::getModuleCode().'\\', __DIR__)
->exclude([__DIR__.'/I18n/*'])
->exclude([__DIR__.'/I18n/*', __DIR__.'/Tests/*'])
->autowire()
->autoconfigure();
}
Expand Down
98 changes: 98 additions & 0 deletions Tests/Integration/AdminOrderCommentSaleUnitScopeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php

declare(strict_types=1);

namespace OrderComment\Tests\Integration;

use OrderComment\Model\OrderComment;
use Thelia\Core\Security\AccessManager;
use Thelia\Core\Security\Resource\AdminResources;
use Thelia\Domain\SaleUnit\Access\AdminSaleUnitScope;
use Thelia\Test\ApiTestCase;

/**
* The admin order comments of an administrator restricted to some sale units are
* those of the orders placed on them.
*/
final class AdminOrderCommentSaleUnitScopeTest extends ApiTestCase
{
private int $mine;
private int $theirs;
private int $national;
private string $restrictedToken;

protected function setUp(): void
{
parent::setUp();

$factory = $this->createFixtureFactory();
$first = $factory->saleUnit();
$second = $factory->saleUnit();
$connection = $this->getPropelConnection();

$myOrder = $factory->order()->setSaleUnitId($first->getId());
$myOrder->save($connection);
$theirOrder = $factory->order()->setSaleUnitId($second->getId());
$theirOrder->save($connection);
$nationalOrder = $factory->order();

$this->mine = $this->comment($myOrder->getId());
$this->theirs = $this->comment($theirOrder->getId());
$this->national = $this->comment($nationalOrder->getId());

$restricted = $factory->restrictedAdmin(
[AdminResources::ORDER => [AccessManager::VIEW]],
['password' => 'password'],
);
$factory->adminSaleUnit($restricted, $first);
$this->restrictedToken = $this->authenticateAsAdmin($restricted);
}

protected function tearDown(): void
{
$this->getService(AdminSaleUnitScope::class)->reset();

parent::tearDown();
}

public function testARestrictedAdministratorReadsTheCommentsOfItsUnitsOnly(): void
{
self::assertSame([$this->mine], $this->listedIds($this->restrictedToken, [$this->mine, $this->theirs, $this->national]));
}

public function testAnAdministratorOfNoUnitReadsEverything(): void
{
$ids = $this->listedIds($this->authenticateAsAdmin(), [$this->mine, $this->theirs, $this->national]);

self::assertSame([$this->mine, $this->theirs, $this->national], $ids);
}

private function comment(int $orderId): int
{
$comment = (new OrderComment())->setOrderId($orderId)->setComment('comment');
$comment->save($this->getPropelConnection());

return $comment->getId();
}

/**
* @param list<int> $among
*
* @return list<int>
*/
private function listedIds(string $token, array $among): array
{
$response = $this->jsonRequest('GET', '/api/admin/order-comments', token: $token, format: 'json');

self::assertSame(200, $response->getStatusCode(), (string) $response->getContent());

$ids = array_map(
static fn (array $row): int => (int) $row['id'],
json_decode((string) $response->getContent(), true, flags: \JSON_THROW_ON_ERROR),
);
$ids = array_values(array_intersect($ids, $among));
sort($ids);

return $ids;
}
}
47 changes: 47 additions & 0 deletions Tests/Integration/AdminResourcesDeclarationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace OrderComment\Tests\Integration;

use OrderComment\Api\Resource\OrderComment as OrderCommentResource;
use OrderComment\OrderComment;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Thelia\Core\Security\Resource\AdminResources;

/**
* The "thelia.api.admin_resources" parameter is shared by every module that publishes
* admin resources: the module adds its own to what is already declared.
*/
final class AdminResourcesDeclarationTest extends TestCase
{
public function testTheResourceIsAddedToWhatAnotherModuleDeclared(): void
{
$container = new ContainerBuilder();
$container->setParameter('thelia.api.admin_resources', ['Other\\Resource' => AdminResources::MODULE]);

foreach (OrderComment::getCompilers() as $pass) {
$pass->process($container);
}

self::assertSame(
['Other\\Resource' => AdminResources::MODULE, OrderCommentResource::class => AdminResources::ORDER],
$container->getParameter('thelia.api.admin_resources'),
);
}

public function testTheResourceIsDeclaredWhenNoOtherModuleDeclaresAny(): void
{
$container = new ContainerBuilder();

foreach (OrderComment::getCompilers() as $pass) {
$pass->process($container);
}

self::assertSame(
[OrderCommentResource::class => AdminResources::ORDER],
$container->getParameter('thelia.api.admin_resources'),
);
}
}
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"license": "GPL-3.0-or-later",
"type": "thelia-module",
"require": {
"thelia/core": ">=3.3.0",
"thelia/installer": "^1.6"
},
"extra": {
Expand Down
Loading