Skip to content

Commit

Permalink
move validation of queries to dto
Browse files Browse the repository at this point in the history
  • Loading branch information
y3n4 committed Apr 22, 2024
1 parent fca0c9e commit 74c99f4
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 88 deletions.
39 changes: 14 additions & 25 deletions src/Controller/Api/AliasController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

namespace App\Controller\Api;

use App\Dto\AliasQueryDto;
use App\Dto\AliasRequestDto;
use App\Dto\PasswordDto;
use App\Dto\LocalpartDto;
use App\Entity\Alias;
use App\Handler\AliasHandler;
use App\Handler\DeleteHandler;
Expand All @@ -14,6 +15,7 @@
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\HttpKernel\Attribute\MapQueryString;

class AliasController extends AbstractController
{
Expand Down Expand Up @@ -57,11 +59,11 @@ public function getAliases(): JsonResponse
*/
#[Route('/api/user/aliases', name: 'post_user_alias', methods: ['POST'])]
public function createRandomAlias(
#[MapRequestPayload] LocalpartDto $dto = new LocalpartDto(null)
#[MapRequestPayload] AliasRequestDto $request = new AliasRequestDto(null)
): JsonResponse {
$user = $this->getUser();
try {
$alias = $this->aliasHandler->create($user, $dto->localpart);
$alias = $this->aliasHandler->create($user, $request->localpart);
} catch (ValidationException $e) {
return $this->json([
'status' => 'failed',
Expand All @@ -76,31 +78,18 @@ public function createRandomAlias(

/**
* Delegates password validation to PasswordDto
* Deletes random user-owned alias with source ${uid} if it exists
*/
#[Route('/api/user/aliases/{uid}', name: 'delete_user_alias', methods: ['DELETE'])]
public function getAlias(#[MapRequestPayload] PasswordDto $dto, string $uid): JsonResponse
{
#[Route('/api/user/aliases', name: 'delete_user_alias', methods: ['DELETE'])]
public function getAlias(
#[MapRequestPayload] PasswordDto $request,
#[MapQueryString] AliasQueryDto $query
): JsonResponse {
$user = $this->getUser();
/**
* Return random aliases owned by authenticated user
* @var Alias $alias
*/
$alias = $this->aliasRepository->findOneByUserAndSource($user, $uid);
if ($alias === null) {
return $this->json([
'status' => 'failed',
'message' => 'forbidden'
], 400);
}
if (!$alias->isRandom()) {
return $this->json([
'status' => 'failed',
'message' => 'contact administrator for custom alias deletion'
], 400);
}
$alias = $this->aliasRepository->findOneByUserAndSource($user, $query->alias);

$this->deleteHandler->deleteAlias($alias);
$dto->erasePassword();

$request->erasePassword();
return $this->json(['status' => 'success'], 200);
}
}
98 changes: 47 additions & 51 deletions src/Controller/Api/WkdController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,23 @@

use App\Dto\PasswordDto;
use App\Dto\WkdRequestDto;
use App\Dto\WkdQueryDto;
use App\Exception\MultipleGpgKeysForUserException;
use App\Exception\NoGpgDataException;
use App\Exception\NoGpgKeyForUserException;
use App\Entity\OpenPgpKey;
use App\Handler\WkdHandler;
use App\Repository\OpenPgpKeyRepository;
use App\Validator\Constraints\WkdQuery;
use App\Validator\Constraints\WkdQueryValidator;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
use RuntimeException;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;

use Symfony\Component\HttpKernel\Attribute\MapQueryString;

class WkdController extends AbstractController
{
Expand All @@ -32,58 +35,52 @@ public function __construct(
$this->repository = $manager->getRepository(OpenPgpKey::class);
}

public function printOpenPgpKey(OpenPgpKey $openPgpKey): array
{
return [
'userId' => $openPgpKey->getEmail(),
'keyId' => $openPgpKey->getKeyId(),
'fingerprint' => $openPgpKey->getKeyFingerprint(),
'expireTime' => $openPgpKey->getKeyExpireTime(),
'uploadedBy' => $openPgpKey->getUser()->__toString(),
];
}

#[Route('/api/user/wkd', methods: ['GET'])]
public function getOpenPgpKeys(): JsonResponse
public function getOpenPgpKey(#[MapQueryString] WkdQueryDto $query = new WkdQueryDto(null, true)): JsonResponse
{
$user = $this->getUser();

$allowedUids = $this->wkdHandler->getAllowedUserIdByUser($user);
$openPgpKeys = $this->repository->findByEmailList($allowedUids);
$keyData = [];
if ($openPgpKeys) {
$keyData = array_map(function (OpenPgpKey $openPgpKey) {
return [
'userId' => $openPgpKey->getEmail(),
'keyId' => $openPgpKey->getKeyId(),
'fingerprint' => $openPgpKey->getKeyFingerprint(),
'expireTime' => $openPgpKey->getKeyExpireTime(),
'uploadedBy' => $openPgpKey->getUser()->__toString(),
];
}, $openPgpKeys);
if ($query->scope || !$query->uid) {
$allowedUids = $this->wkdHandler->getAllowedUserIdByUser($user);
}
return $this->json(['status' => 'success', 'allowedUids' => $allowedUids, 'uploadedKeys' => $keyData], 200);
}

#[Route('/api/user/wkd/{uid}', methods: ['GET'])]
public function getOpenPgpKey(string $uid): JsonResponse
{
if ($error = $this->wkdHandler->entitledToUserId($uid)) {
return $this->json(['status' => 'failed', 'message' => $error], 403);
if ($query->scope) {
$response['allowedUids'] = $allowedUids;
}
$openPgpKey = $this->repository->findByEmail($uid);
$keyData = [];
if ($openPgpKey) {
$keyData = [
'userId' => $openPgpKey->getEmail(),
'keyId' => $openPgpKey->getKeyId(),
'fingerprint' => $openPgpKey->getKeyFingerprint(),
'expireTime' => $openPgpKey->getKeyExpireTime(),
'uploadedBy' => $openPgpKey->getUser()->__toString(),
];

if (!$query->uid) {
if (null != $openPgpKeys = $this->repository->findByEmailList($allowedUids)) {
$response['uploadedKeys'] = array_map(function (OpenPgpKey $openPgpKey) {
return $this->printOpenPgpKey($openPgpKey);
}, $openPgpKeys);
}
} else {
if (null != $openPgpKey = $this->repository->findByEmail($query->uid)) {
$response['uploadedKeys'] = $this->printOpenPgpKey($openPgpKey);
}
}
return $this->json(['status' => 'success', 'keyData' => $keyData], 200);
$response['status'] = 'success';
return $this->json($response, 200);
}

#[Route('/api/user/wkd/{uid}', methods: ['PUT'])]
public function putOpenPgpKey(#[MapRequestPayload] WkdRequestDto $dto, string $uid): JsonResponse
{
#[Route('/api/user/wkd', methods: ['PUT'])]
public function putOpenPgpKey(
#[MapRequestPayload] WkdRequestDto $request,
#[MapQueryString] WkdQueryDto $query
): JsonResponse {
$user = $this->getUser();

if ($error = $this->wkdHandler->entitledToUserId($uid)) {
return $this->json(['status' => 'failed', 'message' => $error], 403);
}
try {
$openpgpkey = $this->wkdHandler->importKey($dto->keydata, $uid, $user);
$openpgpkey = $this->wkdHandler->importKey($request->keydata, $query->uid, $user);
} catch (NoGpgDataException $e) {
return $this->json(['status' => 'failed', 'message' => $e->getMessage()], 400);
} catch (NoGpgKeyForUserException $e) {
Expand All @@ -95,28 +92,27 @@ public function putOpenPgpKey(#[MapRequestPayload] WkdRequestDto $dto, string $u
'keyUid' => $openpgpkey->getKeyId(),
'fingerprint' => $openpgpkey->getKeyFingerprint(),
];
$dto->erasePassword();
$request->erasePassword();
return $this->json(['status' => 'success', 'keyData' => $keyData], 200);
}

/**
* Delegates password validation to AuthenticateDto
*/
#[Route('/api/user/wkd/{uid}', methods: ['DELETE'])]
public function deleteOpenPgpKey(#[MapRequestPayload] PasswordDto $dto, string $uid): JsonResponse
{
if ($error = $this->wkdHandler->entitledToUserId($uid)) {
return $this->json(['status' => 'failed', 'message' => $error], 403);
}
#[Route('/api/user/wkd', methods: ['DELETE'])]
public function deleteOpenPgpKey(
#[MapRequestPayload] WkdRequestDto $request,
#[MapQueryString] WkdQueryDto $query
): JsonResponse {
try {
$deleted = $this->wkdHandler->deleteKey($uid);
$deleted = $this->wkdHandler->deleteKey($query->uid);
} catch (RuntimeException $e) {
return $this->json(['status' => 'failed', 'message' => $e->getMessage()], 400);
}
if (!$deleted) {
return $this->json(['status' => 'failed', 'message' => 'ressource does not exists'], 404);
}
$dto->erasePassword();
$request->erasePassword();

return $this->json(['status' => 'success'], 200);
}
Expand Down
15 changes: 15 additions & 0 deletions src/Dto/AliasQueryDto.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace App\Dto;

use App\Validator\Constraints\AliasDelete;
use Symfony\Component\Validator\Constraints as Assert;

class AliasQueryDto
{
#[Assert\NotNull]
#[Assert\NotBlank]
#[Assert\Email]
#[AliasDelete]
public string $alias;
}
6 changes: 3 additions & 3 deletions src/Dto/LocalpartDto.php → src/Dto/AliasRequestDto.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
namespace App\Dto;

use Symfony\Component\Validator\Constraints as Assert;
use App\Validator\Constraints\CreateAlias;
use App\Validator\Constraints\AliasCreate;

/**
* TODO: move validation for email taken/reserved from aliascreate handler to dto
*/
class LocalpartDto
class AliasRequestDto
{
public function __construct(
#[Assert\NotNull]
#[CreateAlias(
#[AliasCreate(
custom_alias_limit: 3,
random_alias_limit: 100
)]
Expand Down
19 changes: 19 additions & 0 deletions src/Dto/WkdQueryDto.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace App\Dto;

use App\Validator\Constraints\WkdQuery;
use Symfony\Component\Validator\Constraints as Assert;

class WkdQueryDto
{
public function __construct(
#[Assert\NotBlank]
#[Assert\Email]
#[WkdQuery]
public ?string $uid,

public ?bool $scope,
) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
use Symfony\Component\Validator\Exception\MissingOptionsException;

#[\Attribute]
class CreateAlias extends Constraint
class AliasCreate extends Constraint
{
public int $custom_alias_limit = 3;
public int $random_alias_limit = 100;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
namespace App\Validator\Constraints;

use App\Entity\Alias;
use App\Dto\LocalpartDto;
use App\Dto\AliasRequestDto;
use App\Repository\AliasRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Bundle\SecurityBundle\Security;

class CreateAliasValidator extends ConstraintValidator
class AliasCreateValidator extends ConstraintValidator
{
private readonly AliasRepository $repository;

Expand All @@ -27,12 +27,12 @@ public function __construct(
*/
public function validate(mixed $object, Constraint $constraint): void
{
if (!$constraint instanceof CreateAlias) {
throw new UnexpectedTypeException('Wrong constraint type given', CreateAlias::class);
if (!$constraint instanceof AliasCreate) {
throw new UnexpectedTypeException('Wrong constraint type given', AliasCreate::class);
}

if (!$object instanceof LocalpartDto) {
throw new UnexpectedTypeException('Wrong object type given', LocalpartDto::class);
if (!$object instanceof AliasRequestDto) {
throw new UnexpectedTypeException('Wrong object type given', AliasRequestDto::class);
}

$user = $this->security->getUser();
Expand All @@ -46,7 +46,5 @@ public function validate(mixed $object, Constraint $constraint): void
$this->context->addViolation('alias-limit-custom-reached');
}
}


}
}
10 changes: 10 additions & 0 deletions src/Validator/Constraints/AliasDelete.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace App\Validator\Constraints;

use Symfony\Component\Validator\Constraint;

#[\Attribute]
class AliasDelete extends Constraint
{
}
48 changes: 48 additions & 0 deletions src/Validator/Constraints/AliasDeleteValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

namespace App\Validator\Constraints;

use App\Entity\Alias;
use App\Repository\AliasRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Bundle\SecurityBundle\Security;

class AliasDeleteValidator extends ConstraintValidator
{
private readonly AliasRepository $repository;

public function __construct(
private Security $security,
private readonly EntityManagerInterface $manager,
) {
$this->security = $security;
$this->repository = $manager->getRepository(Alias::class);;
}

/**
*/
public function validate(mixed $value, Constraint $constraint): void
{
if (!$constraint instanceof AliasDelete) {
throw new UnexpectedTypeException('Wrong constraint type given', AliasDelete::class);
}

if (!$value instanceof String) {
throw new UnexpectedTypeException('Wrong object type given', String::class);
}

$user = $this->security->getUser();

if (null === $alias = $this->repository->findOneByUserAndSource($user, $value)) {
$this->context->addViolation('forbidden');
return;
}

if (!$alias->isRandom()) {
$this->context->addViolation('not allowed to delete custom alias. contact your system administrator');
}
}
}

0 comments on commit 74c99f4

Please sign in to comment.