diff --git a/Api/Extension/AdminSaleUnitScopeExtension.php b/Api/Extension/AdminSaleUnitScopeExtension.php new file mode 100644 index 0000000..26c50c3 --- /dev/null +++ b/Api/Extension/AdminSaleUnitScopeExtension.php @@ -0,0 +1,65 @@ +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); + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7506d51 --- /dev/null +++ b/CHANGELOG.md @@ -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). diff --git a/Config/module.xml b/Config/module.xml index 78d6305..e039047 100644 --- a/Config/module.xml +++ b/Config/module.xml @@ -7,12 +7,12 @@ Permet au client d'ajouter un commentaire à sa commande - 3.1.0 + 3.1.1 Vincent Lopes-Vicente vlopes@openstudio.fr classic - 3.0.0 + 3.3.0 prod diff --git a/DependencyInjection/AdminApiResourcesPass.php b/DependencyInjection/AdminApiResourcesPass.php new file mode 100644 index 0000000..3aebb5d --- /dev/null +++ b/DependencyInjection/AdminApiResourcesPass.php @@ -0,0 +1,33 @@ +hasParameter(self::PARAMETER) ? (array) $container->getParameter(self::PARAMETER) : []; + + $container->setParameter(self::PARAMETER, [ + ...$declared, + OrderCommentResource::class => AdminResources::ORDER, + ]); + } +} diff --git a/OrderComment.php b/OrderComment.php index f827178..5dcbb0d 100644 --- a/OrderComment.php +++ b/OrderComment.php @@ -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; @@ -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(); } diff --git a/Tests/Integration/AdminOrderCommentSaleUnitScopeTest.php b/Tests/Integration/AdminOrderCommentSaleUnitScopeTest.php new file mode 100644 index 0000000..abab35e --- /dev/null +++ b/Tests/Integration/AdminOrderCommentSaleUnitScopeTest.php @@ -0,0 +1,98 @@ +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 $among + * + * @return list + */ + 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; + } +} diff --git a/Tests/Integration/AdminResourcesDeclarationTest.php b/Tests/Integration/AdminResourcesDeclarationTest.php new file mode 100644 index 0000000..978321f --- /dev/null +++ b/Tests/Integration/AdminResourcesDeclarationTest.php @@ -0,0 +1,47 @@ +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'), + ); + } +} diff --git a/composer.json b/composer.json index 8a86fb2..5161cc6 100644 --- a/composer.json +++ b/composer.json @@ -3,6 +3,7 @@ "license": "GPL-3.0-or-later", "type": "thelia-module", "require": { + "thelia/core": ">=3.3.0", "thelia/installer": "^1.6" }, "extra": {