<?php
/**
* This file is part of the educat package.
*
* (c) Solvee
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace App\Core\Application\Security;
use App\Common\Model\Core\AccountInterface;
use App\Common\Model\Core\UserInterface;
use App\Common\Security\AccountRole;
use App\Common\Security\ObjectOwnerVoterInterface;
use App\Common\Security\ObjectOwnerVoterTrait;
use App\Core\Domain\Entity\Account;
use App\Core\Domain\Entity\User;
use LogicException;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\Security;
/**
* Class AccountVoter
*
* @author Mateusz Korkosz <korkosz.mateusz@gmail.com>
*/
class AccountVoter extends Voter implements ObjectOwnerVoterInterface
{
use ObjectOwnerVoterTrait;
public const EDIT = 'update_account';
public const DELETE = 'delete_account';
public const TOGGLE_NOTIFICATIONS = 'toggle_notifications';
public const GENERATE_ONBOARDING_LINK = 'generate_onboarding_link';
private Security $security;
private RequestStack $requestStack;
/**
* AccountVoter constructor.
*
* @param Security $security
* @param RequestStack $requestStack
*/
public function __construct(Security $security, RequestStack $requestStack)
{
$this->security = $security;
$this->requestStack = $requestStack;
}
/**
* @param string $attribute
* @param mixed $subject
*
* @return bool
*/
protected function supports(mixed $attribute, mixed $subject)
{
if (!in_array(
$attribute,
[
self::EDIT,
self::DELETE,
self::TOGGLE_NOTIFICATIONS,
self::GENERATE_ONBOARDING_LINK,
self::OBJECT_OWNER,
],
true
)) {
return false;
}
return $subject instanceof Account;
}
/**
* @param string $attribute
* @param Account $subject
* @param TokenInterface $token
*
* @return bool
*/
protected function voteOnAttribute(mixed $attribute, mixed $subject, TokenInterface $token)
{
$user = $token->getUser();
if (self::TOGGLE_NOTIFICATIONS === $attribute) {
$nullableUser = $user instanceof UserInterface ?
$user :
null;
return $this->canToggleNotifications($subject, $nullableUser);
}
if (!$user instanceof User) {
return false;
}
if (self::GENERATE_ONBOARDING_LINK === $attribute) {
return $this->canGenerateOnboardingLink($subject, $user);
}
if (self::EDIT === $attribute) {
return $this->canEdit($subject, $user);
}
if (self::DELETE === $attribute) {
return $this->canDelete($subject, $user);
}
if (self::OBJECT_OWNER === $attribute) {
return $this->isObjectOwner($subject, $user->getAccount());
}
throw new LogicException('This code should not be reached!');
}
/**
* @param AccountInterface $subject
* @param UserInterface $user
*
* @return bool
*/
private function canGenerateOnboardingLink(AccountInterface $subject, UserInterface $user): bool
{
return $this->isOwner($subject, $user);
}
/**
* @param AccountInterface $subject
* @param UserInterface|null $user
*
* @return bool
*/
private function canToggleNotifications(AccountInterface $subject, ?UserInterface $user = null): bool
{
if ($this->security->isGranted(AccountRole::ADMIN)) {
return true;
}
if ($user instanceof UserInterface && $user->getAccount() === $subject) {
return true;
}
$request = $this->requestStack->getMasterRequest();
if (!$request) {
return false;
}
$token = $request->headers->get('X-Toggle-Notifications-Token');
$this->throwOnInvalidToken($token, $subject);
return true;
}
/**
* @param Account $account
* @param User $user
*
* @return bool
*/
private function canEdit(Account $account, User $user): bool
{
return $this->isOwner($account, $user);
}
/**
* @param Account $account
* @param User $user
*
* @return bool
*/
private function canDelete(Account $account, User $user): bool
{
return $this->isOwner($account, $user);
}
/**
* @param AccountInterface $account
* @param UserInterface $user
*
* @return bool
*/
private function isOwner(AccountInterface $account, UserInterface $user): bool
{
$accountUser = $account->getUser();
if (!$accountUser instanceof UserInterface) {
return false;
}
return $user->getId()->equals($accountUser->getId()) || $this->security->isGranted(AccountRole::ADMIN);
}
/**
* @param string|null $token
* @param AccountInterface $subject
*/
private function throwOnInvalidToken(?string $token, AccountInterface $subject): void
{
if (!$token || $subject->getNotificationsToggleToken() !== $token) {
throw new BadRequestHttpException('Given token is invalid');
}
}
}