Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Customer #50

Merged
merged 7 commits into from
Sep 18, 2022
6 changes: 5 additions & 1 deletion config/packages/api_platform.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions config/packages/security.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 5 additions & 0 deletions config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
51 changes: 51 additions & 0 deletions src/DataPersister/CustomerDataPersister.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

namespace App\DataPersister;

use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use App\Entity\Customer;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\Security;

final class CustomerDataPersister implements ContextAwareDataPersisterInterface
{
private EntityManagerInterface $entityManager;
private Security $security;

public function __construct(EntityManagerInterface $entityManager, Security $security)
{
$this->entityManager = $entityManager;
$this->security = $security;
}

/**
* @inheritDoc
*/
public function supports($data, array $context = []): bool
{
return $data instanceof Customer;
}

/**
* @inheritDoc
* @param Customer $data
*/
public function persist($data, array $context = [])
{
$user = $this->security->getUser();
$data->setAccount($user->getAccount());
$this->entityManager->persist($data);
$this->entityManager->flush();
return $data;
}

/**
* @inheritDoc
*/
public function remove($data, array $context = [])
{
$this->entityManager->remove($data);
$this->entityManager->flush();
}
}

16 changes: 8 additions & 8 deletions src/DataPersister/UserDataPersister.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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)
Expand All @@ -49,13 +47,15 @@ public function persist($data)
$data->setAccount($user->getAccount());
$this->entityManager->persist($data);
$this->entityManager->flush();
return $data;
}

/**
* @inheritDoc
*/
public function remove($data, array $context = [])
{
$this->dataPersister->remove($data);
$this->entityManager->remove($data);
$this->entityManager->flush();
}
}
2 changes: 1 addition & 1 deletion src/Doctrine/CurrentAccountCustomerExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
9 changes: 3 additions & 6 deletions src/Doctrine/CurrentUserExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,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());
}
}
19 changes: 8 additions & 11 deletions src/Entity/Customer.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,16 @@
#[ORM\Entity(repositoryClass: CustomerRepository::class)]
#[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.',
],
"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'],
Expand All @@ -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)]
Expand All @@ -65,7 +63,6 @@ class Customer
#[ORM\JoinColumn(nullable: false)]
#[Groups(['customer:read'])]
#[Assert\Valid()]
#[Assert\NotBlank()]
private ?Account $account;

#[ORM\Column(type: 'datetime_immutable')]
Expand Down
87 changes: 87 additions & 0 deletions src/OpenApi/JwtDecorator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

declare(strict_types=1);

namespace App\OpenApi;

use ApiPlatform\Core\OpenApi\Factory\OpenApiFactoryInterface;
use ApiPlatform\Core\OpenApi\OpenApi;
use ApiPlatform\Core\OpenApi\Model;

final class JwtDecorator implements OpenApiFactoryInterface
{
public function __construct(
private OpenApiFactoryInterface $decorated
) {}

public function __invoke(array $context = []): OpenApi
{
$openApi = ($this->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' => '[email protected]',
],
'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;
}
}
53 changes: 53 additions & 0 deletions src/Security/Voter/CustomerVoter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace App\Security\Voter;

use App\Entity\Customer;
use Exception;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;

class CustomerVoter extends Voter
{
public const DELETE_CUSTOMER = 'DELETE_CUSTOMER';

private Security $security;

public function __construct(Security $security)
{
$this->security = $security;
}

protected function supports(string $attribute, $subject): bool
{
return $attribute == self::DELETE_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;
}

/** @var Customer $subject */

if ($attribute == self::DELETE_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));
}
}
13 changes: 1 addition & 12 deletions src/Security/Voter/UserVoter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -73,7 +63,6 @@ protected function voteOnAttribute(string $attribute, $subject, TokenInterface $
return true;
}
return false;
break;
}

throw new Exception(sprintf('Unhandled attribute "%s"', $attribute));
Expand Down
Loading