CVE-2021-32716: Incorrect Authorization (CWE-863)
value: Shopware is an open source eCommerce platform. In versions prior to 6.4.1.1 the admin api has exposed some internal hidden fields when an association has been loaded with a to many reference. Users are recommend to update to version 6.4.1.1. You can get the update to 6.4.1.1 regularly via the Auto-Updater or directly via the download overview. For older versions of 6.1, 6.2, and 6.3, corresponding security measures are also available via a plugin.
CWE description: The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. This allows attackers to bypass intended access restrictions.
CVSS v3 base score: 4.9
Published: 2021-06-24T21:15Z
Repository: https://github.com/shopware/platform
Language: PHP
Files changed: .gitlab-ci.yml, dangerfile.js, src/Core/Checkout/DependencyInjection/order.xml, src/Core/Checkout/Order/SalesChannel/CancelOrderRoute.php, src/Core/Checkout/Test/Order/SalesChannel/CancelOrderRouteTest.php, src/Core/Framework/Test/TestCaseBase/BasicTestDataBehaviour.php
Commit date: 2021-06-08 10:17:07 +0200
Commit message: NEXT-15183 - Add Dangerfile

Diff: diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 5f860c1fd9..06433ad6c8 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -57,10 +57,18 @@ default:
         - /entrypoint supervisord > /dev/null 2>&1 &
     interruptible: true
 
 # stage: unit
 
+Danger:
+    stage: unit
+    image: node:14
+    before_script: []
+    script:
+        - npm install danger gitlab
+        - node node_modules/.bin/danger ci --base trunk
+
 PHP Full:
     image: $DEV_IMAGE
     stage: unit
     services:
         -   name: $MYSQL_IMAGE
diff --git a/dangerfile.js b/dangerfile.js
new file mode 100644
index 0000000000..3aa2bfe4d8
--- /dev/null
+++ b/dangerfile.js
@@ -0,0 +1,54 @@
+const GitlabClient = require('gitlab');
+
+const api = new GitlabClient.Gitlab({
+    token: process.env['DANGER_GITLAB_API_TOKEN'],
+    host: process.env['DANGER_GITLAB_HOST']
+});
+
+const addLabel = (labels) => {
+    const currentLabels = danger.gitlab.mr.labels;
+
+    for (const label of labels) {
+        currentLabels.push(label);
+    }
+
+    api.MergeRequests.edit(1, process.env['CI_MERGE_REQUEST_IID'], {
+        'labels': currentLabels.join(',')
+    })
+};
+
+const removeLabel = (labels) => {
+    const currentLabels = danger.gitlab.mr.labels;
+
+    for (const label of labels) {
+        const index = currentLabels.indexOf(label);
+        if (index > -1) {
+            currentLabels.splice(index, 1);
+        }
+
+        api.MergeRequests.edit(1, process.env['CI_MERGE_REQUEST_IID'], {
+            'labels': currentLabels.join(',')
+        })
+    }
+};
+
+const hasStoreApiRouteChanges = () => {
+    for (let file of danger.git.modified_files) {
+        if (file.includes('SalesChannel') && file.includes('Route.php') && !file.includes('/Test/')) {
+            return true;
+        }
+    }
+
+    for (let file of danger.git.created_files) {
+        if (file.includes('SalesChannel') && file.includes('Route.php') && !file.includes('/Test/')) {
+            return true;
+        }
+    }
+
+    return false;
+}
+
+if (hasStoreApiRouteChanges()) {
+    warn('Store-API Route has been modified. @Reviewers please review carefully!')
+    addLabel(['Security-Audit Required']);
+}
diff --git a/src/Core/Checkout/DependencyInjection/order.xml b/src/Core/Checkout/DependencyInjection/order.xml
index a80920a1cc..ca5afd7a73 100644
--- a/src/Core/Checkout/DependencyInjection/order.xml
+++ b/src/Core/Checkout/DependencyInjection/order.xml
@@ -93,10 +93,11 @@
             <argument type="service" id="promotion.repository"/>
         </service>
 
         <service id="Shopware\Core\Checkout\Order\SalesChannel\CancelOrderRoute" public="true">
             <argument type="service" id="Shopware\Core\Checkout\Order\SalesChannel\OrderService"/>
+            <argument type="service" id="order.repository"/>
         </service>
 
         <service id="Shopware\Core\Checkout\Order\SalesChannel\SetPaymentOrderRoute" public="true">
             <argument type="service" id="Shopware\Core\Checkout\Order\SalesChannel\OrderService"/>
             <argument type="service" id="order.repository"/>
diff --git a/src/Core/Checkout/Order/SalesChannel/CancelOrderRoute.php b/src/Core/Checkout/Order/SalesChannel/CancelOrderRoute.php
index e2f2c84846..ef8867d186 100644
--- a/src/Core/Checkout/Order/SalesChannel/CancelOrderRoute.php
+++ b/src/Core/Checkout/Order/SalesChannel/CancelOrderRoute.php
@@ -1,32 +1,38 @@
 <?php declare(strict_types=1);
 
 namespace Shopware\Core\Checkout\Order\SalesChannel;
 
 use OpenApi\Annotations as OA;
+use Shopware\Core\Checkout\Cart\Exception\CustomerNotLoggedInException;
+use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
+use Shopware\Core\Framework\DataAbstractionLayer\Exception\EntityNotFoundException;
+use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
+use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
 use Shopware\Core\Framework\Plugin\Exception\DecorationPatternException;
 use Shopware\Core\Framework\Routing\Annotation\LoginRequired;
 use Shopware\Core\Framework\Routing\Annotation\RouteScope;
 use Shopware\Core\Framework\Routing\Annotation\Since;
+use Shopware\Core\Framework\Routing\Exception\InvalidRequestParameterException;
 use Shopware\Core\System\SalesChannel\SalesChannelContext;
 use Symfony\Component\HttpFoundation\ParameterBag;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\Routing\Annotation\Route;
 
 /**
  * @RouteScope(scopes={"store-api"})
  */
 class CancelOrderRoute extends AbstractCancelOrderRoute
 {
-    /**
-     * @var OrderService
-     */
-    private $orderService;
+    private OrderService $orderService;
+
+    private EntityRepositoryInterface $orderRepository;
 
-    public function __construct(OrderService $orderService)
+    public function __construct(OrderService $orderService, EntityRepositoryInterface $orderRepository)
     {
         $this->orderService = $orderService;
+        $this->orderRepository = $orderRepository;
     }
 
     public function getDecorated(): AbstractCancelOrderRoute
     {
         throw new DecorationPatternException(self::class);
@@ -54,15 +60,37 @@ class CancelOrderRoute extends AbstractCancelOrderRoute
      * @LoginRequired()
      * @Route(path="/store-api/order/state/cancel", name="store-api.order.state.cancel", methods={"POST"})
      */
     public function cancel(Request $request, SalesChannelContext $context): CancelOrderRouteResponse
     {
+        $orderId = $request->get('orderId', null);
+
+        if ($orderId === null) {
+            throw new InvalidRequestParameterException('orderId');
+        }
+
+        $this->verify($orderId, $context);
+
         $newState = $this->orderService->orderStateTransition(
-            $request->get('orderId'),
+            $orderId,
             'cancel',
             new ParameterBag(),
             $context->getContext()
         );
 
         return new CancelOrderRouteResponse($newState);
     }
+
+    private function verify(string $orderId, SalesChannelContext $context): void
+    {
+        if ($context->getCustomer() === null) {
+            throw new CustomerNotLoggedInException();
+        }
+
+        $criteria = new Criteria([$orderId]);
+        $criteria->addFilter(new EqualsFilter('orderCustomer.customerId', $context->getCustomer()->getId()));
+
+        if ($this->orderRepository->searchIds($criteria, $context->getContext())->firstId() === null) {
+            throw new EntityNotFoundException('order', $orderId);
+        }
+    }
 }
diff --git a/src/Core/Checkout/Test/Order/SalesChannel/CancelOrderRouteTest.php b/src/Core/Checkout/Test/Order/SalesChannel/CancelOrderRouteTest.php
new file mode 100644
index 0000000000..0ace519ad1
--- /dev/null
+++ b/src/Core/Checkout/Test/Order/SalesChannel/CancelOrderRouteTest.php
@@ -0,0 +1,229 @@
+<?php declare(strict_types=1);
+
+namespace Shopware\Core\Checkout\Test\Order\SalesChannel;
+
+use PHPUnit\Framework\TestCase;
+use Shopware\Core\Checkout\Cart\LineItem\LineItem;
+use Shopware\Core\Checkout\Cart\Price\Struct\CalculatedPrice;
+use Shopware\Core\Checkout\Cart\Price\Struct\CartPrice;
+use Shopware\Core\Checkout\Cart\Price\Struct\QuantityPriceDefinition;
+use Shopware\Core\Checkout\Cart\Tax\Struct\CalculatedTaxCollection;
+use Shopware\Core\Checkout\Cart\Tax\Struct\TaxRuleCollection;
+use Shopware\Core\Checkout\Order\OrderEntity;
+use Shopware\Core\Checkout\Test\Customer\SalesChannel\CustomerTestTrait;
+use Shopware\Core\Defaults;
+use Shopware\Core\Framework\Context;
+use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
+use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
+use Shopware\Core\Framework\Test\IdsCollection;
+use Shopware\Core\Framework\Test\TestCaseBase\IntegrationTestBehaviour;
+use Shopware\Core\Framework\Test\TestDataCollection;
+use Shopware\Core\Framework\Uuid\Uuid;
+use Symfony\Bundle\FrameworkBundle\KernelBrowser;
+use Symfony\Component\HttpFoundation\Response;
+
+class CancelOrderRouteTest extends TestCase
+{
+    use IntegrationTestBehaviour;
+    use CustomerTestTrait;
+
+    private KernelBrowser $browser;
+
+    private EntityRepositoryInterface $customerRepository;
+
+    private IdsCollection $ids;
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+
+        $this->customerRepository = $this->getContainer()->get('customer.repository');
+        $this->ids = new TestDataCollection();
+
+        $this->browser = $this->createCustomSalesChannelBrowser([
+            'id' => $this->ids->create('sales-channel'),
+        ]);
+
+        $this->assignSalesChannelContext($this->browser);
+
+        $email = Uuid::randomHex() . '@example.com';
+        $customerId = $this->createCustomer('shopware', $email);
+
+        $this->ids->set('order-1', $this->createOrder($this->ids, $customerId));
+        $this->ids->set('order-2', $this->createOrder($this->ids, $this->createCustomer('test', 'test-other@test.de')));
+
+        $this->browser
+            ->request(
+                'POST',
+                '/store-api/account/login',
+                [
+                    'email' => $email,
+                    'password' => 'shopware',
+                ]
+            );
+
+        $response = json_decode($this->browser->getResponse()->getContent(), true);
+
+        $this->browser->setServerParameter('HTTP_SW_CONTEXT_TOKEN', $response['contextToken']);
+    }
+
+    public function testCancelMyOwnOrder(): void
+    {
+        $this->browser
+            ->request(
+                'POST',
+                '/store-api/order/state/cancel',
+                [
+                    'orderId' => $this->ids->get('order-1'),
+                ]
+            );
+
+        $response = json_decode($this->browser->getResponse()->getContent(), true);
+
+        static::assertSame(Response::HTTP_OK, $this->browser->getResponse()->getStatusCode());
+        static::assertSame('cancelled', $response['technicalName']);
+
+        $criteria = new Criteria([$this->ids->get('order-1')]);
+        $criteria->addAssociation('stateMachineState');
+
+        /** @var OrderEntity $order */
+        $order = $this->getContainer()->get('order.repository')->search($criteria, Context::createDefaultContext())->first();
+
+        static::assertSame('cancelled', $order->getStateMachineState()->getTechnicalName());
+    }
+
+    public function testCancelRandomOrder(): void
+    {
+        $this->browser
+            ->request(
+                'POST',
+                '/store-api/order/state/cancel',
+                [
+                    'orderId' => Uuid::randomHex(),
+                ]
+            );
+
+        $response = json_decode($this->browser->getResponse()->getContent(), true);
+
+        static::assertSame(Response::HTTP_NOT_FOUND, $this->browser->getResponse()->getStatusCode());
+        static::assertSame('FRAMEWORK__ENTITY_NOT_FOUND', $response['errors'][0]['code']);
+    }
+
+    public function testCancelOtherUsersOrder(): void
+    {
+        $this->browser
+            ->request(
+                'POST',
+                '/store-api/order/state/cancel',
+                [
+                    'orderId' => $this->ids->get('order-2'),
+                ]
+            );
+
+        $response = json_decode($this->browser->getResponse()->getContent(), true);
+
+        static::assertSame(Response::HTTP_NOT_FOUND, $this->browser->getResponse()->getStatusCode());
+        static::assertSame('FRAMEWORK__ENTITY_NOT_FOUND', $response['errors'][0]['code']);
+    }
+
+    public function testCancelWithoutLogin(): void
+    {
+        $this->browser = $this->createCustomSalesChannelBrowser([
+            'id' => Uuid::randomHex(),
+        ]);
+
+        $this->browser
+            ->request(
+                'POST',
+                '/store-api/order/state/cancel',
+                [
+                    'orderId' => $this->ids->get('order-2'),
+                ]
+            );
+
+        $response = json_decode($this->browser->getResponse()->getContent(), true);
+
+        static::assertSame(Response::HTTP_FORBIDDEN, $this->browser->getResponse()->getStatusCode());
+        static::assertSame('CHECKOUT__CUSTOMER_NOT_LOGGED_IN', $response['errors'][0]['code']);
+    }
+
+    private function createOrder(IdsCollection $ids, string $customerId): string
+    {
+        $id = Uuid::randomHex();
+
+        $this->getContainer()->get('order.repository')->create(
+            [[
+                'id' => $id,
+                'orderDateTime' => (new \DateTimeImmutable())->format(Defaults::STORAGE_DATE_TIME_FORMAT),
+                'price' => new CartPrice(10, 10, 10, new CalculatedTaxCollection(), new TaxRuleCollection(), CartPrice::TAX_STATE_NET),
+                'shippingCosts' => new CalculatedPrice(10, 10, new CalculatedTaxCollection(), new TaxRuleCollection()),
+                'orderCustomer' => [
+                    'customerId' => $customerId,
+                    'email' => 'test@example.com',
+                    'salutationId' => $this->getValidSalutationId(),
+                    'firstName' => 'Max',
+                    'lastName' => 'Mustermann',
+                ],
+                'stateId' => $this->getStateMachineState(),
+                'paymentMethodId' => $this->getValidPaymentMethodId(),
+                'currencyId' => Defaults::CURRENCY,
+                'currencyFactor' => 1.0,
+                'salesChannelId' => Defaults::SALES_CHANNEL,
+                'billingAddressId' => $billingAddressId = Uuid::randomHex(),
+                'addresses' => [
+                    [
+                        'id' => $billingAddressId,
+                        'salutationId' => $this->getValidSalutationId(),
+                        'firstName' => 'Max',
+                        'lastName' => 'Mustermann',
+                        'street' => 'Ebbinghoff 10',
+                        'zipcode' => '48624',
+                        'city' => 'Schöppingen',
+                        'countryId' => $this->getValidCountryId(),
+                    ],
+                ],
+                'lineItems' => [
+                    [
+                        'id' => $ids->get('VoucherA'),
+                        'type' => LineItem::PROMOTION_LINE_ITEM_TYPE,
+                        'code' => $ids->get('VoucherA'),
+                        'identifier' => $ids->get('VoucherA'),
+                        'quantity' => 1,
+                        'payload' => ['promotionId' => $ids->get('voucherA')],
+                        'label' => 'label',
+                        'price' => new CalculatedPrice(200, 200, new CalculatedTaxCollection(), new TaxRuleCollection()),
+                        'priceDefinition' => new QuantityPriceDefinition(200, new TaxRuleCollection(), 2),
+                    ],
+                    [
+                        'id' => $ids->get('VoucherC'),
+                        'type' => LineItem::PROMOTION_LINE_ITEM_TYPE,
+                        'code' => $ids->get('VoucherC'),
+                        'identifier' => $ids->get('VoucherC'),
+                        'payload' => ['promotionId' => $ids->get('voucherB')],
+                        'quantity' => 1,
+                        'label' => 'label',
+                        'price' => new CalculatedPrice(200, 200, new CalculatedTaxCollection(), new TaxRuleCollection()),
+                        'priceDefinition' => new QuantityPriceDefinition(200, new TaxRuleCollection(), 2),
+                    ],
+                    [
+                        'id' => $ids->get('VoucherB'),
+                        'type' => LineItem::PROMOTION_LINE_ITEM_TYPE,
+                        'code' => $ids->get('VoucherB'),
+                        'identifier' => $ids->get('VoucherB'),
+                        'payload' => ['promotionId' => $ids->get('voucherB')],
+                        'quantity' => 1,
+                        'label' => 'label',
+                        'price' => new CalculatedPrice(200, 200, new CalculatedTaxCollection(), new TaxRuleCollection()),
+                        'priceDefinition' => new QuantityPriceDefinition(200, new TaxRuleCollection(), 2),
+                    ],
+                ],
+                'deliveries' => [],
+                'context' => '{}',
+                'payload' => '{}',
+            ]],
+            Context::createDefaultContext()
+        );
+
+        return $id;
+    }
+}
diff --git a/src/Core/Framework/Test/TestCaseBase/BasicTestDataBehaviour.php b/src/Core/Framework/Test/TestCaseBase/BasicTestDataBehaviour.php
index 62602995eb..4b43eb1c9e 100644
--- a/src/Core/Framework/Test/TestCaseBase/BasicTestDataBehaviour.php
+++ b/src/Core/Framework/Test/TestCaseBase/BasicTestDataBehaviour.php
@@ -1,9 +1,10 @@
 <?php declare(strict_types=1);
 
 namespace Shopware\Core\Framework\Test\TestCaseBase;
 
+use Shopware\Core\Checkout\Order\OrderStates;
 use Shopware\Core\Checkout\Payment\PaymentMethodEntity;
 use Shopware\Core\Checkout\Shipping\ShippingMethodEntity;
 use Shopware\Core\Defaults;
 use Shopware\Core\Framework\Context;
 use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
@@ -178,6 +179,20 @@ trait BasicTestDataBehaviour
 
         $criteria = (new Criteria())->setLimit(1);
 
         return $repository->searchIds($criteria, Context::createDefaultContext())->getIds()[0];
     }
+
+    protected function getStateMachineState(string $stateMachine = OrderStates::STATE_MACHINE, string $state = OrderStates::STATE_OPEN): string
+    {
+        /** @var EntityRepositoryInterface $repository */
+        $repository = $this->getContainer()->get('state_machine_state.repository');
+
+        $criteria = new Criteria();
+        $criteria
+            ->setLimit(1)
+            ->addFilter(new EqualsFilter('technicalName', $state))
+            ->addFilter(new EqualsFilter('stateMachine.technicalName', $stateMachine));
+
+        return $repository->searchIds($criteria, Context::createDefaultContext())->getIds()[0];
+    }
 }

Vulnerable code:     /**
     * @var OrderService
     */
    private $orderService;
    public function __construct(OrderService $orderService)
            $request->get('orderId'),
Fixed code: Danger:
    stage: unit
    image: node:14
    before_script: []
    script:
        - npm install danger gitlab
        - node node_modules/.bin/danger ci --base trunk

const GitlabClient = require('gitlab');

const api = new GitlabClient.Gitlab({
    token: process.env['DANGER_GITLAB_API_TOKEN'],
    host: process.env['DANGER_GITLAB_HOST']
});

const addLabel = (labels) => {
    const currentLabels = danger.gitlab.mr.labels;

    for (const label of labels) {
        currentLabels.push(label);
    }

    api.MergeRequests.edit(1, process.env['CI_MERGE_REQUEST_IID'], {
        'labels': currentLabels.join(',')
    })
};

const removeLabel = (labels) => {
    const currentLabels = danger.gitlab.mr.labels;

    for (const label of labels) {
        const index = currentLabels.indexOf(label);
        if (index > -1) {
            currentLabels.splice(index, 1);
        }

        api.MergeRequests.edit(1, process.env['CI_MERGE_REQUEST_IID'], {
            'labels': currentLabels.join(',')
        })
    }
};

const hasStoreApiRouteChanges = () => {
    for (let file of danger.git.modified_files) {
        if (file.includes('SalesChannel') && file.includes('Route.php') && !file.includes('/Test/')) {
            return true;
        }
    }

    for (let file of danger.git.created_files) {
        if (file.includes('SalesChannel') && file.includes('Route.php') && !file.includes('/Test/')) {
            return true;
        }
    }

    return false;
}

if (hasStoreApiRouteChanges()) {
    warn('Store-API Route has been modified. @Reviewers please review carefully!')
    addLabel(['Security-Audit Required']);
}
            <argument type="service" id="order.repository"/>
use Shopware\Core\Checkout\Cart\Exception\CustomerNotLoggedInException;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Exception\EntityNotFoundException;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\Routing\Exception\InvalidRequestParameterException;
    private OrderService $orderService;

    private EntityRepositoryInterface $orderRepository;
    public function __construct(OrderService $orderService, EntityRepositoryInterface $orderRepository)
        $this->orderRepository = $orderRepository;
        $orderId = $request->get('orderId', null);

        if ($orderId === null) {
            throw new InvalidRequestParameterException('orderId');
        }

        $this->verify($orderId, $context);

            $orderId,

    private function verify(string $orderId, SalesChannelContext $context): void
    {
        if ($context->getCustomer() === null) {
            throw new CustomerNotLoggedInException();
        }

        $criteria = new Criteria([$orderId]);
        $criteria->addFilter(new EqualsFilter('orderCustomer.customerId', $context->getCustomer()->getId()));

        if ($this->orderRepository->searchIds($criteria, $context->getContext())->firstId() === null) {
            throw new EntityNotFoundException('order', $orderId);
        }
    }
<?php declare(strict_types=1);

namespace Shopware\Core\Checkout\Test\Order\SalesChannel;

use PHPUnit\Framework\TestCase;
use Shopware\Core\Checkout\Cart\LineItem\LineItem;
use Shopware\Core\Checkout\Cart\Price\Struct\CalculatedPrice;
use Shopware\Core\Checkout\Cart\Price\Struct\CartPrice;
use Shopware\Core\Checkout\Cart\Price\Struct\QuantityPriceDefinition;
use Shopware\Core\Checkout\Cart\Tax\Struct\CalculatedTaxCollection;
use Shopware\Core\Checkout\Cart\Tax\Struct\TaxRuleCollection;
use Shopware\Core\Checkout\Order\OrderEntity;
use Shopware\Core\Checkout\Test\Customer\SalesChannel\CustomerTestTrait;
use Shopware\Core\Defaults;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\Test\IdsCollection;
use Shopware\Core\Framework\Test\TestCaseBase\IntegrationTestBehaviour;
use Shopware\Core\Framework\Test\TestDataCollection;
use Shopware\Core\Framework\Uuid\Uuid;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Component\HttpFoundation\Response;

class CancelOrderRouteTest extends TestCase
{
    use IntegrationTestBehaviour;
    use CustomerTestTrait;

    private KernelBrowser $browser;

    private EntityRepositoryInterface $customerRepository;

    private IdsCollection $ids;

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

        $this->customerRepository = $this->getContainer()->get('customer.repository');
        $this->ids = new TestDataCollection();

        $this->browser = $this->createCustomSalesChannelBrowser([
            'id' => $this->ids->create('sales-channel'),
        ]);

        $this->assignSalesChannelContext($this->browser);

        $email = Uuid::randomHex() . '@example.com';
        $customerId = $this->createCustomer('shopware', $email);

        $this->ids->set('order-1', $this->createOrder($this->ids, $customerId));
        $this->ids->set('order-2', $this->createOrder($this->ids, $this->createCustomer('test', 'test-other@test.de')));

        $this->browser
            ->request(
                'POST',
                '/store-api/account/login',
                [
                    'email' => $email,
                    'password' => 'shopware',
                ]
            );

        $response = json_decode($this->browser->getResponse()->getContent(), true);

        $this->browser->setServerParameter('HTTP_SW_CONTEXT_TOKEN', $response['contextToken']);
    }

    public function testCancelMyOwnOrder(): void
    {
        $this->browser
            ->request(
                'POST',
                '/store-api/order/state/cancel',
                [
                    'orderId' => $this->ids->get('order-1'),
                ]
            );

        $response = json_decode($this->browser->getResponse()->getContent(), true);

        static::assertSame(Response::HTTP_OK, $this->browser->getResponse()->getStatusCode());
        static::assertSame('cancelled', $response['technicalName']);

        $criteria = new Criteria([$this->ids->get('order-1')]);
        $criteria->addAssociation('stateMachineState');

        /** @var OrderEntity $order */
        $order = $this->getContainer()->get('order.repository')->search($criteria, Context::createDefaultContext())->first();

        static::assertSame('cancelled', $order->getStateMachineState()->getTechnicalName());
    }

    public function testCancelRandomOrder(): void
    {
        $this->browser
            ->request(
                'POST',
                '/store-api/order/state/cancel',
                [
                    'orderId' => Uuid::randomHex(),
                ]
            );

        $response = json_decode($this->browser->getResponse()->getContent(), true);

        static::assertSame(Response::HTTP_NOT_FOUND, $this->browser->getResponse()->getStatusCode());
        static::assertSame('FRAMEWORK__ENTITY_NOT_FOUND', $response['errors'][0]['code']);
    }

    public function testCancelOtherUsersOrder(): void
    {
        $this->browser
            ->request(
                'POST',
                '/store-api/order/state/cancel',
                [
                    'orderId' => $this->ids->get('order-2'),
                ]
            );

        $response = json_decode($this->browser->getResponse()->getContent(), true);

        static::assertSame(Response::HTTP_NOT_FOUND, $this->browser->getResponse()->getStatusCode());
        static::assertSame('FRAMEWORK__ENTITY_NOT_FOUND', $response['errors'][0]['code']);
    }

    public function testCancelWithoutLogin(): void
    {
        $this->browser = $this->createCustomSalesChannelBrowser([
            'id' => Uuid::randomHex(),
        ]);

        $this->browser
            ->request(
                'POST',
                '/store-api/order/state/cancel',
                [
                    'orderId' => $this->ids->get('order-2'),
                ]
            );

        $response = json_decode($this->browser->getResponse()->getContent(), true);

        static::assertSame(Response::HTTP_FORBIDDEN, $this->browser->getResponse()->getStatusCode());
        static::assertSame('CHECKOUT__CUSTOMER_NOT_LOGGED_IN', $response['errors'][0]['code']);
    }

    private function createOrder(IdsCollection $ids, string $customerId): string
    {
        $id = Uuid::randomHex();

        $this->getContainer()->get('order.repository')->create(
            [[
                'id' => $id,
                'orderDateTime' => (new \DateTimeImmutable())->format(Defaults::STORAGE_DATE_TIME_FORMAT),
                'price' => new CartPrice(10, 10, 10, new CalculatedTaxCollection(), new TaxRuleCollection(), CartPrice::TAX_STATE_NET),
                'shippingCosts' => new CalculatedPrice(10, 10, new CalculatedTaxCollection(), new TaxRuleCollection()),
                'orderCustomer' => [
                    'customerId' => $customerId,
                    'email' => 'test@example.com',
                    'salutationId' => $this->getValidSalutationId(),
                    'firstName' => 'Max',
                    'lastName' => 'Mustermann',
                ],
                'stateId' => $this->getStateMachineState(),
                'paymentMethodId' => $this->getValidPaymentMethodId(),
                'currencyId' => Defaults::CURRENCY,
                'currencyFactor' => 1.0,
                'salesChannelId' => Defaults::SALES_CHANNEL,
                'billingAddressId' => $billingAddressId = Uuid::randomHex(),
                'addresses' => [
                    [
                        'id' => $billingAddressId,
                        'salutationId' => $this->getValidSalutationId(),
                        'firstName' => 'Max',
                        'lastName' => 'Mustermann',
                        'street' => 'Ebbinghoff 10',
                        'zipcode' => '48624',
                        'city' => 'Schöppingen',
                        'countryId' => $this->getValidCountryId(),
                    ],
                ],
                'lineItems' => [
                    [
                        'id' => $ids->get('VoucherA'),
                        'type' => LineItem::PROMOTION_LINE_ITEM_TYPE,
                        'code' => $ids->get('VoucherA'),
                        'identifier' => $ids->get('VoucherA'),
                        'quantity' => 1,
                        'payload' => ['promotionId' => $ids->get('voucherA')],
                        'label' => 'label',
                        'price' => new CalculatedPrice(200, 200, new CalculatedTaxCollection(), new TaxRuleCollection()),
                        'priceDefinition' => new QuantityPriceDefinition(200, new TaxRuleCollection(), 2),
                    ],
                    [
                        'id' => $ids->get('VoucherC'),
                        'type' => LineItem::PROMOTION_LINE_ITEM_TYPE,
                        'code' => $ids->get('VoucherC'),
                        'identifier' => $ids->get('VoucherC'),
                        'payload' => ['promotionId' => $ids->get('voucherB')],
                        'quantity' => 1,
                        'label' => 'label',
                        'price' => new CalculatedPrice(200, 200, new CalculatedTaxCollection(), new TaxRuleCollection()),
                        'priceDefinition' => new QuantityPriceDefinition(200, new TaxRuleCollection(), 2),
                    ],
                    [
                        'id' => $ids->get('VoucherB'),
                        'type' => LineItem::PROMOTION_LINE_ITEM_TYPE,
                        'code' => $ids->get('VoucherB'),
                        'identifier' => $ids->get('VoucherB'),
                        'payload' => ['promotionId' => $ids->get('voucherB')],
                        'quantity' => 1,
                        'label' => 'label',
                        'price' => new CalculatedPrice(200, 200, new CalculatedTaxCollection(), new TaxRuleCollection()),
                        'priceDefinition' => new QuantityPriceDefinition(200, new TaxRuleCollection(), 2),
                    ],
                ],
                'deliveries' => [],
                'context' => '{}',
                'payload' => '{}',
            ]],
            Context::createDefaultContext()
        );

        return $id;
    }
}
use Shopware\Core\Checkout\Order\OrderStates;

    protected function getStateMachineState(string $stateMachine = OrderStates::STATE_MACHINE, string $state = OrderStates::STATE_OPEN): string
    {
        /** @var EntityRepositoryInterface $repository */
        $repository = $this->getContainer()->get('state_machine_state.repository');

        $criteria = new Criteria();
        $criteria
            ->setLimit(1)
            ->addFilter(new EqualsFilter('technicalName', $state))
            ->addFilter(new EqualsFilter('stateMachine.technicalName', $stateMachine));

        return $repository->searchIds($criteria, Context::createDefaultContext())->getIds()[0];
    }