From 43c97607753500495ad1e2dfe2dd1e34688c5ed7 Mon Sep 17 00:00:00 2001 From: EstelleMyddleware Date: Thu, 8 Sep 2022 19:43:20 +0200 Subject: [PATCH 1/6] feat: insert JWT auth form input on Swagger UI to be able to access the test requests --- config/packages/api_platform.yaml | 6 +++++- config/packages/security.yaml | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/config/packages/api_platform.yaml b/config/packages/api_platform.yaml index 2ae82ba..3361b89 100644 --- a/config/packages/api_platform.yaml +++ b/config/packages/api_platform.yaml @@ -6,7 +6,11 @@ api_platform: formats: jsonld: [ 'application/ld+json' ] json: [ 'application/json' ] -# swagger: + swagger: + api_keys: + JWT: + name: Authorization + type: header # versions: [3] enable_swagger_ui: true enable_re_doc: true \ No newline at end of file diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 9f5e75e..74ef5c1 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -64,6 +64,7 @@ security: - { path: ^/api/(login|token/refresh), roles: PUBLIC_ACCESS } # - { path: ^/api/token/refresh, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/api/docs, roles: PUBLIC_ACCESS } + - { path: ^/docs, roles: PUBLIC_ACCESS } - { path: ^/api, roles: ROLE_USER } when@test: From 60231b6ae25a8364d34e568a71faee9ac1d3f1f7 Mon Sep 17 00:00:00 2001 From: EstelleMyddleware Date: Thu, 8 Sep 2022 21:16:45 +0200 Subject: [PATCH 2/6] fix: Current User can only access users from their own account, thanks to Doctrine Query extension --- src/Doctrine/CurrentUserExtension.php | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Doctrine/CurrentUserExtension.php b/src/Doctrine/CurrentUserExtension.php index 7c4763e..bbfc20c 100644 --- a/src/Doctrine/CurrentUserExtension.php +++ b/src/Doctrine/CurrentUserExtension.php @@ -35,15 +35,12 @@ public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterf private function addWhere(QueryBuilder $queryBuilder, string $resourceClass): void { - if (Account::class !== $resourceClass || $this->security->isGranted('ROLE_SUPER_ADMIN') || null === $user = $this->security->getUser()) { + if (User::class !== $resourceClass || $this->security->isGranted('ROLE_SUPER_ADMIN') || null === $user = $this->security->getUser()) { return; } - - //@TODO: this doesn't actually work; the request is probably wrong - // the goal is to only return the current user's account instead of all accounts + // the goal is to only return the current user's account users instead of all users $rootAlias = $queryBuilder->getRootAliases()[0]; - $queryBuilder->andWhere('u.account = :user_account'); - $queryBuilder->innerJoin(User::class, 'u', Join::WITH, sprintf('u.account = %s.id', $rootAlias)); + $queryBuilder->andWhere(sprintf('%s.account = :user_account', $rootAlias)); $queryBuilder->setParameter('user_account', $user->getAccount()); } From 5d2665d5109666bb90fb8b938e720c1630f895b9 Mon Sep 17 00:00:00 2001 From: EstelleMyddleware Date: Sat, 17 Sep 2022 16:34:31 +0200 Subject: [PATCH 3/6] docs: Add JWT token login_check endpoint on SwaggerUI --- config/services.yaml | 5 +++ src/OpenApi/JwtDecorator.php | 87 ++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 src/OpenApi/JwtDecorator.php diff --git a/config/services.yaml b/config/services.yaml index 892f2fe..ec08539 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -23,6 +23,11 @@ services: - '../src/Entity/' - '../src/Kernel.php' + + App\OpenApi\JwtDecorator: + decorates: 'api_platform.openapi.factory' + arguments: [ '@.inner' ] + # App\DataPersister\UserDataPersister: # bind: # $dataPersister: '@api_platform.doctrine.orm.data_persister' diff --git a/src/OpenApi/JwtDecorator.php b/src/OpenApi/JwtDecorator.php new file mode 100644 index 0000000..d682a09 --- /dev/null +++ b/src/OpenApi/JwtDecorator.php @@ -0,0 +1,87 @@ +decorated)($context); + $schemas = $openApi->getComponents()->getSchemas(); + + $schemas['Token'] = new \ArrayObject([ + 'type' => 'object', + 'properties' => [ + 'token' => [ + 'type' => 'string', + 'readOnly' => true, + ], + ], + ]); + $schemas['Credentials'] = new \ArrayObject([ + 'type' => 'object', + 'properties' => [ + 'username' => [ + 'type' => 'string', + 'example' => 'johndoe@example.com', + ], + 'password' => [ + 'type' => 'string', + 'example' => 'apassword', + ], + ], + ]); + + $schemas = $openApi->getComponents()->getSecuritySchemes() ?? []; + $schemas['JWT'] = new \ArrayObject([ + 'type' => 'http', + 'scheme' => 'bearer', + 'bearerFormat' => 'JWT', + ]); + + $pathItem = new Model\PathItem( + ref: 'JWT Token', + post: new Model\Operation( + operationId: 'postCredentialsItem', + tags: ['Token'], + responses: [ + '200' => [ + 'description' => 'Get JWT token', + 'content' => [ + 'application/json' => [ + 'schema' => [ + '$ref' => '#/components/schemas/Token', + ], + ], + ], + ], + ], + summary: 'Get JWT token to login.', + requestBody: new Model\RequestBody( + description: 'Generate new JWT Token', + content: new \ArrayObject([ + 'application/json' => [ + 'schema' => [ + '$ref' => '#/components/schemas/Credentials', + ], + ], + ]), + ), + security: [], + ), + ); + $openApi->getPaths()->addPath('/api/login_check', $pathItem); + + return $openApi; + } +} \ No newline at end of file From 5f5879106a3f4aad85ebb0a096cbc321b45f6032 Mon Sep 17 00:00:00 2001 From: EstelleMyddleware Date: Sat, 17 Sep 2022 20:43:48 +0200 Subject: [PATCH 4/6] fix: assign Customer to current user Account on creation with datapersister --- src/DataPersister/CustomerDataPersister.php | 52 +++++++++++++++++++ .../CurrentAccountCustomerExtension.php | 2 +- src/Entity/Customer.php | 5 +- src/Security/Voter/CustomerVoter.php | 51 ++++++++++++++++++ src/Security/Voter/UserVoter.php | 13 +---- src/Test/CustomApiTestCase.php | 20 +++++++ tests/Functional/CustomerTest.php | 34 +++++++++++- 7 files changed, 161 insertions(+), 16 deletions(-) create mode 100644 src/DataPersister/CustomerDataPersister.php create mode 100644 src/Security/Voter/CustomerVoter.php diff --git a/src/DataPersister/CustomerDataPersister.php b/src/DataPersister/CustomerDataPersister.php new file mode 100644 index 0000000..d7ab09a --- /dev/null +++ b/src/DataPersister/CustomerDataPersister.php @@ -0,0 +1,52 @@ +entityManager = $entityManager; + $this->dataPersister = $dataPersister; + $this->security = $security; + } + + /** + * @inheritDoc + */ + public function supports($data): bool + { + return $data instanceof Customer; + } + + /** + * @inheritDoc + * @param Customer $data + */ + public function persist($data) + { + $user = $this->security->getUser(); + $data->setAccount($user->getAccount()); + $this->entityManager->persist($data); + $this->entityManager->flush(); + } + + /** + * @inheritDoc + */ + public function remove($data, array $context = []) + { + $this->dataPersister->remove($data); + } +} + diff --git a/src/Doctrine/CurrentAccountCustomerExtension.php b/src/Doctrine/CurrentAccountCustomerExtension.php index 37f626d..edf28b5 100644 --- a/src/Doctrine/CurrentAccountCustomerExtension.php +++ b/src/Doctrine/CurrentAccountCustomerExtension.php @@ -36,7 +36,7 @@ private function addWhere(QueryBuilder $queryBuilder, string $resourceClass): vo // only return Customers from current user's Account $rootAlias = $queryBuilder->getRootAliases()[0]; - $queryBuilder->andWhere(sprintf('%s.account = :current_account', $rootAlias)); + $queryBuilder->andWhere(sprintf('%s.account = :user_account', $rootAlias)); $queryBuilder->setParameter('user_account', $user->getAccount()); } } diff --git a/src/Entity/Customer.php b/src/Entity/Customer.php index 86a8a59..4428f61 100644 --- a/src/Entity/Customer.php +++ b/src/Entity/Customer.php @@ -14,8 +14,9 @@ #[ApiResource( collectionOperations: [ "get" => [ - 'security' => 'is_granted("ROLE_ADMIN") or object.getAccount() == user.getAccount()', - 'security_message' => 'Sorry, you can only access Customers linked to your own Account.', + // 'security' => 'is_granted("ROLE_ADMIN") or object.getAccount() == user.getAccount()', + 'security' => 'is_granted("VIEW_CUSTOMER", object)', + 'security_message' => 'So sorry, you can only access Customers linked to your own Account.', ], "post" ], diff --git a/src/Security/Voter/CustomerVoter.php b/src/Security/Voter/CustomerVoter.php new file mode 100644 index 0000000..d7f3ed9 --- /dev/null +++ b/src/Security/Voter/CustomerVoter.php @@ -0,0 +1,51 @@ +security = $security; + } + + protected function supports(string $attribute, $subject): bool + { + return $attribute == self::VIEW_CUSTOMER + && $subject instanceof Customer; + } + + /** + * @throws Exception + */ + protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool + { + $user = $token->getUser(); + // if the user is anonymous, do not grant access + if (!$user instanceof UserInterface) { + return false; + } + + if ($attribute == self::VIEW_CUSTOMER) { + if ($subject->getAccount() === $user->getAccount()) { + return true; + } + if ($this->security->isGranted('ROLE_ADMIN')) { + return true; + } + return false; + } + throw new Exception(sprintf('Unhandled attribute "%s"', $attribute)); + } +} diff --git a/src/Security/Voter/UserVoter.php b/src/Security/Voter/UserVoter.php index 1e3b079..f537b40 100644 --- a/src/Security/Voter/UserVoter.php +++ b/src/Security/Voter/UserVoter.php @@ -46,25 +46,15 @@ protected function voteOnAttribute(string $attribute, $subject, TokenInterface $ /** @var User $subject */ switch ($attribute) { - case self::VIEW: - if ($subject->getAccount() === $user->getAccount()) { - return true; - } - if ($this->security->isGranted('ROLE_ADMIN')) { - return true; - } - return false; - break; case self::CREATE: + case self::VIEW: if ($subject->getAccount() === $user->getAccount()) { return true; } - if ($this->security->isGranted('ROLE_ADMIN')) { return true; } return false; - break; case self::DELETE: if ($subject === $user) { return true; @@ -73,7 +63,6 @@ protected function voteOnAttribute(string $attribute, $subject, TokenInterface $ return true; } return false; - break; } throw new Exception(sprintf('Unhandled attribute "%s"', $attribute)); diff --git a/src/Test/CustomApiTestCase.php b/src/Test/CustomApiTestCase.php index d8c803d..42d7931 100644 --- a/src/Test/CustomApiTestCase.php +++ b/src/Test/CustomApiTestCase.php @@ -5,6 +5,7 @@ use ApiPlatform\Core\Bridge\Symfony\Bundle\Test\ApiTestCase; use ApiPlatform\Core\Bridge\Symfony\Bundle\Test\Client; use App\Entity\Account; +use App\Entity\Customer; use App\Entity\User; use DateTimeImmutable; use Doctrine\ORM\EntityManagerInterface; @@ -54,6 +55,25 @@ public function createAccount(string $primaryEmail): Account return $account; } + /** + * @throws Exception + */ + public function createCustomer(string $firstName, string $lastName, string $email, string $phoneNumber, Account $account): Customer + { + $em = $this->getEntityManager(); + $customer = new Customer(); + $customer->setFirstName($firstName); + $customer->setLastName($lastName); + $customer->setEmail($email); + $customer->setPhoneNumber($phoneNumber); + $customer->setAccount($account); + $customer->setCreatedAt(new DateTimeImmutable()); + $customer->setUpdatedAt(new DateTimeImmutable()); + + $em->persist($customer); + $em->flush(); + return $customer; + } /** * @coversNothing * @throws TransportExceptionInterface diff --git a/tests/Functional/CustomerTest.php b/tests/Functional/CustomerTest.php index 0f5a5e4..e89b20f 100644 --- a/tests/Functional/CustomerTest.php +++ b/tests/Functional/CustomerTest.php @@ -46,12 +46,38 @@ public function testUserCannotCreateCustomersWithoutJWT() { } + /** + * @throws Exception + * @throws TransportExceptionInterface + */ public function testUserCanDeleteCustomerOnOwnAccount() { + $client = self::createClient(); + $container = static::getContainer(); + $account = $this->createAccount('marie.dupont@fnac.com'); + $user = $this->createUser('vincent.rock@gmail.com', 'camomille', $account); + $customer = $this->createCustomer( + 'Elodie', + 'Lacour', + 'elodie.lacour@gmail.com', + '06 02 04 04 03', + $account + ); + $token = $this->getJWTToken($user, $client,'camomille'); + $response = $client->request('DELETE', '/api/customers/'.$customer->getId(), [ + 'headers' => [ + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer '.$token + ] + ]); + $this->assertResponseIsSuccessful(); + $this->assertResponseStatusCodeSame(204); + } public function testUserCannotDeleteCustomerOnDifferentAccount() { + } /** @@ -64,7 +90,13 @@ public function testUserCannotDeleteCustomerWithoutAuth() $container = static::getContainer(); $account = $this->createAccount('charles.laborde@fnac.com'); $user = $this->createUser('louise.vandenbeck@gmail.com', 'banana', $account); - $customer = $this->createCustomer(); + $customer = $this->createCustomer( + 'Kévin', + 'Leblanc', + 'kevin.leblanc@gmail.com', + '07 06 05 04 03', + $account + ); $response = $client->request('DELETE', '/api/customers/'.$customer->getId(), [ 'headers' => [ 'Content-Type' => 'application/json', From c51f5aaf235f2c638040e49a93198144ce67369e Mon Sep 17 00:00:00 2001 From: EstelleMyddleware Date: Sun, 18 Sep 2022 20:34:55 +0200 Subject: [PATCH 5/6] fix: switch to ContextAwareDataPersisterInterface & remove validation constraint on $account in Customer resource --- src/DataPersister/CustomerDataPersister.php | 15 ++++++--------- src/DataPersister/UserDataPersister.php | 14 ++++++-------- src/Entity/Customer.php | 6 ++---- src/Security/Voter/CustomerVoter.php | 4 +++- 4 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/DataPersister/CustomerDataPersister.php b/src/DataPersister/CustomerDataPersister.php index d7ab09a..6535fc9 100644 --- a/src/DataPersister/CustomerDataPersister.php +++ b/src/DataPersister/CustomerDataPersister.php @@ -2,29 +2,26 @@ namespace App\DataPersister; -use ApiPlatform\Core\DataPersister\DataPersisterInterface; +use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface; use App\Entity\Customer; use Doctrine\ORM\EntityManagerInterface; -use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Security\Core\Security; -final class CustomerDataPersister implements DataPersisterInterface +final class CustomerDataPersister implements ContextAwareDataPersisterInterface { private EntityManagerInterface $entityManager; - private DataPersisterInterface $dataPersister; private Security $security; - public function __construct(EntityManagerInterface $entityManager, DataPersisterInterface $dataPersister, Security $security) + public function __construct(EntityManagerInterface $entityManager, Security $security) { $this->entityManager = $entityManager; - $this->dataPersister = $dataPersister; $this->security = $security; } /** * @inheritDoc */ - public function supports($data): bool + public function supports($data, array $context = []): bool { return $data instanceof Customer; } @@ -33,7 +30,7 @@ public function supports($data): bool * @inheritDoc * @param Customer $data */ - public function persist($data) + public function persist($data, array $context = []) { $user = $this->security->getUser(); $data->setAccount($user->getAccount()); @@ -46,7 +43,7 @@ public function persist($data) */ public function remove($data, array $context = []) { - $this->dataPersister->remove($data); + $this->entityManager->remove($data); } } diff --git a/src/DataPersister/UserDataPersister.php b/src/DataPersister/UserDataPersister.php index 12d4859..39add24 100644 --- a/src/DataPersister/UserDataPersister.php +++ b/src/DataPersister/UserDataPersister.php @@ -2,31 +2,29 @@ namespace App\DataPersister; -use ApiPlatform\Core\DataPersister\DataPersisterInterface; +use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface; use App\Entity\User; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Security\Core\Security; -final class UserDataPersister implements DataPersisterInterface +final class UserDataPersister implements ContextAwareDataPersisterInterface { private EntityManagerInterface $entityManager; private UserPasswordHasherInterface $userPasswordHasher; - private DataPersisterInterface $dataPersister; private Security $security; - public function __construct(EntityManagerInterface $entityManager, UserPasswordHasherInterface $userPasswordHasher, DataPersisterInterface $dataPersister, Security $security) + public function __construct(EntityManagerInterface $entityManager, UserPasswordHasherInterface $userPasswordHasher, Security $security) { $this->entityManager = $entityManager; $this->userPasswordHasher = $userPasswordHasher; - $this->dataPersister = $dataPersister; $this->security = $security; } /** * @inheritDoc */ - public function supports($data): bool + public function supports($data, $context = []): bool { return $data instanceof User; } @@ -35,7 +33,7 @@ public function supports($data): bool * @inheritDoc * @param User $data */ - public function persist($data) + public function persist($data, $context = []) { // this only applies in the context of API requests, when persisting an Entity manually, // the traditional method prevails (setting $password to a hashed pwd manually) @@ -56,6 +54,6 @@ public function persist($data) */ public function remove($data, array $context = []) { - $this->dataPersister->remove($data); + $this->entityManager->remove($data); } } diff --git a/src/Entity/Customer.php b/src/Entity/Customer.php index 4428f61..84298b6 100644 --- a/src/Entity/Customer.php +++ b/src/Entity/Customer.php @@ -14,9 +14,8 @@ #[ApiResource( collectionOperations: [ "get" => [ - // 'security' => 'is_granted("ROLE_ADMIN") or object.getAccount() == user.getAccount()', - 'security' => 'is_granted("VIEW_CUSTOMER", object)', - 'security_message' => 'So sorry, you can only access Customers linked to your own Account.', + "security" => "is_granted('VIEW_CUSTOMER', object)", + "security_message" => "So sorry, you can only access Customers linked to your own Account.", ], "post" ], @@ -66,7 +65,6 @@ class Customer #[ORM\JoinColumn(nullable: false)] #[Groups(['customer:read'])] #[Assert\Valid()] - #[Assert\NotBlank()] private ?Account $account; #[ORM\Column(type: 'datetime_immutable')] diff --git a/src/Security/Voter/CustomerVoter.php b/src/Security/Voter/CustomerVoter.php index d7f3ed9..8b40e1f 100644 --- a/src/Security/Voter/CustomerVoter.php +++ b/src/Security/Voter/CustomerVoter.php @@ -29,7 +29,7 @@ protected function supports(string $attribute, $subject): bool /** * @throws Exception */ - protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool + protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool { $user = $token->getUser(); // if the user is anonymous, do not grant access @@ -37,6 +37,8 @@ protected function voteOnAttribute(string $attribute, $subject, TokenInterface $ return false; } + /** @var Customer $subject */ + if ($attribute == self::VIEW_CUSTOMER) { if ($subject->getAccount() === $user->getAccount()) { return true; From 57afd097885b58eda8c54fc0187d89588caa5545 Mon Sep 17 00:00:00 2001 From: EstelleMyddleware Date: Sun, 18 Sep 2022 21:44:32 +0200 Subject: [PATCH 6/6] fix: https://github.com/EstelleMyddleware/bilemo/issues/51 & https://github.com/EstelleMyddleware/bilemo/issues/52 flush entitymanager after remove operation & remove redundant voter --- src/DataPersister/CustomerDataPersister.php | 2 ++ src/DataPersister/UserDataPersister.php | 2 ++ src/Entity/Customer.php | 18 ++++++++---------- src/Security/Voter/CustomerVoter.php | 6 +++--- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/DataPersister/CustomerDataPersister.php b/src/DataPersister/CustomerDataPersister.php index 6535fc9..ccb138c 100644 --- a/src/DataPersister/CustomerDataPersister.php +++ b/src/DataPersister/CustomerDataPersister.php @@ -36,6 +36,7 @@ public function persist($data, array $context = []) $data->setAccount($user->getAccount()); $this->entityManager->persist($data); $this->entityManager->flush(); + return $data; } /** @@ -44,6 +45,7 @@ public function persist($data, array $context = []) public function remove($data, array $context = []) { $this->entityManager->remove($data); + $this->entityManager->flush(); } } diff --git a/src/DataPersister/UserDataPersister.php b/src/DataPersister/UserDataPersister.php index 39add24..4e22346 100644 --- a/src/DataPersister/UserDataPersister.php +++ b/src/DataPersister/UserDataPersister.php @@ -47,6 +47,7 @@ public function persist($data, $context = []) $data->setAccount($user->getAccount()); $this->entityManager->persist($data); $this->entityManager->flush(); + return $data; } /** @@ -55,5 +56,6 @@ public function persist($data, $context = []) public function remove($data, array $context = []) { $this->entityManager->remove($data); + $this->entityManager->flush(); } } diff --git a/src/Entity/Customer.php b/src/Entity/Customer.php index 84298b6..fa31122 100644 --- a/src/Entity/Customer.php +++ b/src/Entity/Customer.php @@ -13,19 +13,16 @@ #[ORM\Entity(repositoryClass: CustomerRepository::class)] #[ApiResource( collectionOperations: [ - "get" => [ - "security" => "is_granted('VIEW_CUSTOMER', object)", - "security_message" => "So sorry, you can only access Customers linked to your own Account.", - ], + "get", "post" ], itemOperations: [ - "get" => [ - 'security' => 'is_granted("ROLE_ADMIN") or object.getAccount() == user.getAccount()', - 'security_message' => 'Sorry, you can only access Customers linked to your own Account.', - ], - "delete" => ["security" => "is_granted('ROLE_ADMIN') or object.getAccount() == user.getAccount()"], - ], + "get", + "delete" => [ + "security" => "is_granted('DELETE_CUSTOMER', object)", + 'security_message' => 'Sorry, you can only delete Customers linked to your own Account.', + ], + ], attributes: [ 'pagination_items_per_page' => 10, 'formats' => ['json', 'jsonld'], @@ -39,6 +36,7 @@ class Customer #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(type: 'integer')] + #[Groups(['customer:read'])] private ?int $id; #[ORM\Column(type: 'string', length: 255, unique: true)] diff --git a/src/Security/Voter/CustomerVoter.php b/src/Security/Voter/CustomerVoter.php index 8b40e1f..0c13493 100644 --- a/src/Security/Voter/CustomerVoter.php +++ b/src/Security/Voter/CustomerVoter.php @@ -11,7 +11,7 @@ class CustomerVoter extends Voter { - public const VIEW_CUSTOMER = 'VIEW_CUSTOMER'; + public const DELETE_CUSTOMER = 'DELETE_CUSTOMER'; private Security $security; @@ -22,7 +22,7 @@ public function __construct(Security $security) protected function supports(string $attribute, $subject): bool { - return $attribute == self::VIEW_CUSTOMER + return $attribute == self::DELETE_CUSTOMER && $subject instanceof Customer; } @@ -39,7 +39,7 @@ protected function voteOnAttribute(string $attribute, $subject, TokenInterface /** @var Customer $subject */ - if ($attribute == self::VIEW_CUSTOMER) { + if ($attribute == self::DELETE_CUSTOMER) { if ($subject->getAccount() === $user->getAccount()) { return true; }