feature: add Authenticator

This commit is contained in:
Jan Klattenhoff 2024-01-18 12:01:43 +01:00
parent b102519154
commit 2380c13c37
15 changed files with 1370 additions and 17 deletions

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PhpTestFrameworkVersionCache">
<tools_cache>
<tool tool_name="PHPUnit">
<cache>
<versions>
<info id="LocalC:\Users\jklattenhoff\Documents\symfony_crud\vendor\autoload.php" version="10.5.7" />
</versions>
</cache>
</tool>
</tools_cache>
</component>
</project>

@ -89,6 +89,14 @@
<path value="$PROJECT_DIR$/vendor/phar-io/version" />
<path value="$PROJECT_DIR$/vendor/phar-io/manifest" />
<path value="$PROJECT_DIR$/vendor/myclabs/deep-copy" />
<path value="$PROJECT_DIR$/vendor/symfony/serializer" />
<path value="$PROJECT_DIR$/vendor/symfony/property-info" />
<path value="$PROJECT_DIR$/vendor/symfony/property-access" />
<path value="$PROJECT_DIR$/vendor/phpstan/phpdoc-parser" />
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-docblock" />
<path value="$PROJECT_DIR$/vendor/phpdocumentor/type-resolver" />
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-common" />
<path value="$PROJECT_DIR$/vendor/webmozart/assert" />
</include_path>
</component>
<component name="PhpInterpreters">

@ -11,6 +11,9 @@
<PhpSpecSuiteConfiguration>
<option name="myPath" value="$PROJECT_DIR$" />
</PhpSpecSuiteConfiguration>
<PhpSpecSuiteConfiguration>
<option name="myPath" value="$PROJECT_DIR$" />
</PhpSpecSuiteConfiguration>
</suites>
</component>
</project>

@ -26,6 +26,10 @@
<excludeFolder url="file://$MODULE_DIR$/vendor/nikic/php-parser" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phar-io/manifest" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phar-io/version" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpdocumentor/reflection-common" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpdocumentor/reflection-docblock" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpdocumentor/type-resolver" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpstan/phpdoc-parser" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-code-coverage" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-file-iterator" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-invoker" />
@ -74,8 +78,11 @@
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-mbstring" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php83" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/process" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/property-access" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/property-info" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/routing" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/runtime" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/serializer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/service-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/stopwatch" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/string" />
@ -83,6 +90,7 @@
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/var-exporter" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/yaml" />
<excludeFolder url="file://$MODULE_DIR$/vendor/theseer/tokenizer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/webmozart/assert" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />

@ -10,11 +10,17 @@
"doctrine/doctrine-bundle": "^2.11",
"doctrine/doctrine-migrations-bundle": "^3.3",
"doctrine/orm": "^2.17",
"phpdocumentor/reflection-docblock": "^5.3",
"phpstan/phpdoc-parser": "^1.25",
"symfony/console": "7.0.*",
"symfony/dotenv": "7.0.*",
"symfony/flex": "^2",
"symfony/framework-bundle": "7.0.*",
"symfony/property-access": "7.0.*",
"symfony/property-info": "7.0.*",
"symfony/runtime": "7.0.*",
"symfony/security-bundle": "7.0.*",
"symfony/serializer": "7.0.*",
"symfony/yaml": "7.0.*"
},
"config": {

1072
composer.lock generated

File diff suppressed because it is too large Load Diff

@ -5,4 +5,5 @@ return [
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
];

@ -0,0 +1,40 @@
security:
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
users_in_memory: { memory: null }
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: users_in_memory
custom_authenticator: App\Security\PrinterAuthenticator
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }
when@test:
security:
password_hashers:
# By default, password hashers are resource intensive and take time. This is
# important to generate secure password hashes. In tests however, secure hashes
# are not important, waste resources and increase test times. The following
# reduces the work factor to the lowest possible values.
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
algorithm: auto
cost: 4 # Lowest possible value for bcrypt
time_cost: 3 # Lowest possible value for argon
memory_cost: 10 # Lowest possible value for argon

@ -0,0 +1,3 @@
_security_logout:
resource: security.route_loader.logout
type: service

@ -3,32 +3,86 @@
namespace App\Controller;
use App\Entity\Printer;
use App\Enum\ErrorMessages;
use App\Repository\PrinterRepository;
use App\Service\PrinterService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class PrinterCrudController extends AbstractController
{
public function __construct(private readonly PrinterRepository $printerRepository)
public function __construct(
private readonly PrinterRepository $printerRepository,
private readonly PrinterService $printerService,
)
{
}
#[Route('/printer', name: 'printer')]
public function printer(): JsonResponse
#[Route('/printer', name: 'printer', methods: ['GET'])]
public function getAllPrinters(): JsonResponse
{
return $this->json($this->printerRepository->findAll());
}
#[Route('/printer/{id}', name: 'app_printer', methods: ["GET"])]
public function index(?Printer $printer): JsonResponse
#[Route('/printer/{id}', name: 'get_printer', methods: ['GET'])]
public function getPrinter(?Printer $printer): JsonResponse
{
if (!$printer) {
return $this->json([
'message' => 'printer does not exist',
'message' => ErrorMessages::DOESNT_EXIST->value,
]);
}
return $this->json($printer);
}
#[Route('/printer/{id}', name: 'delete_printer', methods: ['DELETE'])]
public function deletePrinter(?Printer $printer): JsonResponse
{
if (!$printer) {
return $this->json([
'message' => ErrorMessages::DOESNT_EXIST->value,
]);
}
$this->printerService->deletePrinter($printer);
return $this->json([
'message' => 'printer was deleted',
]);
}
#[Route('/printer', name: 'create_printer', methods: ['POST'])]
public function createPrinter(Request $request): JsonResponse
{
$jsonContent = $request->getContent();
if (!$this->printerService->validateJson($jsonContent)) {
return $this->json([
'message' => ErrorMessages::DATA_INCOMPLETE->value,
]);
}
$printer = $this->printerService->createPrinter($jsonContent);
return $this->json($printer);
}
#[Route('/printer/{id}', name: 'edit_printer', methods: ['PUT'])]
public function editPrinter(?Printer $printer, Request $request): JsonResponse
{
if (!$printer) {
return $this->json([
'message' => ErrorMessages::DOESNT_EXIST->value,
]);
}
if (!$this->printerService->validateJson($request->getContent())) {
return $this->json([
'message' => ErrorMessages::DATA_INCOMPLETE,
]);
}
}
}

@ -0,0 +1,9 @@
<?php
namespace App\Enum;
enum ErrorMessages: string
{
case DATA_INCOMPLETE = 'The provided data is incomplete';
case DOESNT_EXIST = 'printer does not exist';
}

@ -0,0 +1,44 @@
<?php
namespace App\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
class PrinterAuthenticator extends AbstractAuthenticator
{
public function supports(Request $request): ?bool
{
// TODO: Implement supports() method.
}
public function authenticate(Request $request): Passport
{
// TODO: Implement authenticate() method.
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
// TODO: Implement onAuthenticationSuccess() method.
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
// TODO: Implement onAuthenticationFailure() method.
}
// public function start(Request $request, AuthenticationException $authException = null): Response
// {
// /*
// * If you would like this class to control what happens when an anonymous user accesses a
// * protected page (e.g. redirect to /login), uncomment this method and make this class
// * implement Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface.
// *
// * For more details, see https://symfony.com/doc/current/security/experimental_authenticators.html#configuring-the-authentication-entry-point
// */
// }
}

@ -1,20 +1,35 @@
<?php
<?php declare(strict_types=1);
namespace App\Service;
use App\Entity\Printer;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\Entity;
use Symfony\Component\Serializer\SerializerInterface;
class PrinterService
{
public function __construct(private readonly EntityManagerInterface $entityManager)
public function __construct(private readonly EntityManagerInterface $entityManager, private readonly SerializerInterface $serializer)
{
}
public function deletePrinter(Printer $printer)
public function deletePrinter(Printer $printer): void
{
$this->entityManager->remove($printer);
$this->entityManager->flush();
}
public function createPrinter(string $jsonString): Printer
{
$printer = $this->serializer->deserialize($jsonString, Printer::class, 'json');
$this->entityManager->persist($printer);
$this->entityManager->flush();
return $printer;
}
public function validateJson(string $jsonString): bool
{
$array = json_decode($jsonString, true);
return isset($array['name'], $array['price'], $array['max_speed'], $array['build_volume']);
}
}

@ -104,5 +104,18 @@
"./config/packages/routing.yaml",
"./config/routes.yaml"
]
},
"symfony/security-bundle": {
"version": "7.0",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.4",
"ref": "2ae08430db28c8eb4476605894296c82a642028f"
},
"files": [
"config/packages/security.yaml",
"config/routes/security.yaml"
]
}
}

@ -2,17 +2,82 @@
namespace App\Tests\Service;
use App\Entity\Printer;
use App\Service\PrinterService;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\Attributes\Test;
use PrinterService;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\MakerBundle\Docker\DockerDatabaseServices;
use Symfony\Component\Serializer\SerializerInterface;
use function PHPUnit\Framework\once;
class PrinterServiceTest extends TestCase
{
#[Test]
public function EditPrinterShouldEditPrinter(){
$printerService = new PrinterService();
private readonly EntityManagerInterface&MockObject $entityManager;
private readonly SerializerInterface&MockObject $serializer;
private readonly PrinterService $printerService;
$printerService->editPrinter(Printer $printer, )
protected function setUp(): void
{
$this->entityManager = self::createMock(EntityManagerInterface::class);
$this->serializer = self::createMock(SerializerInterface::class);
$this->printerService = new PrinterService($this->entityManager, $this->serializer);
}
#[Test]
public function removeShouldInvokeRemoveOnPrinterAndFlush()
{
$printer = new Printer();
$this->entityManager->expects(self::once())->method('remove')->with($printer);
$this->entityManager->expects(self::once())->method('flush');
$this->printerService->deletePrinter($printer);
}
#[Test]
public function validateJsonShouldReturnFalseOnInputIncompleteJson()
{
$invalidJson = '{
"name": "Bambu A1"
}';
self::assertFalse($this->printerService->validateJson($invalidJson));
}
#[Test]
public function validateJsonShouldReturnTrueOnInputCompleteJson()
{
$validJson = '{
"name": "Bambu A1 Mini",
"price": 10.50,
"build_volume": "180x180x180",
"max_speed": 1000
}';
self::assertTrue($this->printerService->validateJson($validJson));
}
#[Test]
public function createPrinterShouldInvokePersistAndFlushOnCreatedPrinterObject(){
$json = '{
"name": "Bambu A1 Mini",
"price": 10.50,
"build_volume": "180x180x180",
"max_speed": 1000
}';
$printer = new Printer();
$printer
->setName('Bambu A1 Mini')
->setPrice(10.50)
->setBuildVolume('180x180x180')
->setMaxSpeed(1000);
$this->serializer->expects(self::once())->method('deserialize')->with($json)->willReturn($printer);
$this->entityManager->expects(self::once())->method('persist')->with($printer);
$this->entityManager->expects(self::once())->method('flush');
$this->printerService->createPrinter($json);
}
}