diff --git a/src/Voter/AliasVoter.php b/src/Voter/AliasVoter.php
index f5690f6a6..d5b512986 100644
--- a/src/Voter/AliasVoter.php
+++ b/src/Voter/AliasVoter.php
@@ -4,6 +4,8 @@
use App\Entity\Alias;
use App\Entity\User;
+use App\Enum\Roles;
+use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
@@ -11,6 +13,10 @@ class AliasVoter extends Voter
{
public const DELETE = 'delete';
+ public function __construct(private readonly Security $security)
+ {
+ }
+
protected function supports(string $attribute, mixed $subject): bool
{
if ($attribute !== self::DELETE) {
@@ -27,18 +33,28 @@ protected function supports(string $attribute, mixed $subject): bool
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
$user = $token->getUser();
- $alias = $subject;
-
- $isUserValid = $user instanceof User;
- $isAliasValid = $alias instanceof Alias;
- $isNotDeleted = $isAliasValid && !$alias->isDeleted();
- $isRandom = $isAliasValid && $alias->isRandom();
- $isOwner = $isAliasValid && $alias->getUser() === $user;
-
- return $isUserValid
- && $isAliasValid
- && $isNotDeleted
- && $isRandom
- && $isOwner;
+ $alias = $subject; // already ensured to be Alias by supports()
+
+ if (!$user instanceof User || !$alias instanceof Alias) {
+ return false; // sanity check
+ }
+
+ if ($alias->isDeleted()) {
+ return false; // cannot delete already deleted
+ }
+
+ $isOwner = $alias->getUser() === $user;
+
+ // owner can delete own random alias
+ if ($alias->isRandom() && $isOwner) {
+ return true;
+ }
+
+ // ADMIN or DOMAIN_ADMIN can delete their own custom aliases
+ if ($isOwner && ($this->security->isGranted(Roles::ADMIN) || $this->security->isGranted(Roles::DOMAIN_ADMIN))) {
+ return true;
+ }
+
+ return false;
}
}
diff --git a/templates/Alias/show.html.twig b/templates/Alias/show.html.twig
index deb1624f1..3b980090a 100644
--- a/templates/Alias/show.html.twig
+++ b/templates/Alias/show.html.twig
@@ -116,16 +116,27 @@
{% for alias in aliases_custom %}
{{ alias.source }}
-
+
{% endfor %}
{% endif %}
diff --git a/tests/Voter/AliasVoterTest.php b/tests/Voter/AliasVoterTest.php
index c732dd9b1..fa84c9bb3 100644
--- a/tests/Voter/AliasVoterTest.php
+++ b/tests/Voter/AliasVoterTest.php
@@ -4,7 +4,9 @@
use App\Entity\Alias;
use App\Entity\User;
+use App\Enum\Roles;
use App\Voter\AliasVoter;
+use Symfony\Component\Security\Core\Security;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
@@ -12,32 +14,32 @@ class AliasVoterTest extends TestCase
{
public function testSupportsReturnsTrueForDeleteAndAlias(): void
{
- $voter = new AliasVoter();
$alias = new Alias();
+ $voter = new AliasVoter($this->createSecurity(false));
$this->assertTrue($this->invokeSupports($voter, AliasVoter::DELETE, $alias));
}
public function testSupportsReturnsFalseForOtherAttribute(): void
{
- $voter = new AliasVoter();
$alias = new Alias();
+ $voter = new AliasVoter($this->createSecurity(false));
$this->assertFalse($this->invokeSupports($voter, 'OTHER_ATTRIBUTE', $alias));
}
public function testSupportsReturnsFalseForNonAliasSubject(): void
{
- $voter = new AliasVoter();
$user = new User();
+ $voter = new AliasVoter($this->createSecurity(false));
$this->assertFalse($this->invokeSupports($voter, AliasVoter::DELETE, $user));
}
public function testVoteOnAttributeReturnsTrueIfUserOwnsAlias(): void
{
- $voter = new AliasVoter();
$user = new User();
$alias = new Alias();
$alias->setRandom(true);
$alias->setUser($user);
+ $voter = new AliasVoter($this->createSecurity(false));
$token = $this->createMock(TokenInterface::class);
$token->method('getUser')->willReturn($user);
$this->assertTrue($this->invokeVoteOnAttribute($voter, AliasVoter::DELETE, $alias, $token));
@@ -45,12 +47,12 @@ public function testVoteOnAttributeReturnsTrueIfUserOwnsAlias(): void
public function testVoteOnAttributeReturnsFalseIfUserDoesNotOwnAlias(): void
{
- $voter = new AliasVoter();
$user = new User();
$otherUser = new User();
$alias = new Alias();
$alias->setRandom(true);
$alias->setUser($otherUser);
+ $voter = new AliasVoter($this->createSecurity(false));
$token = $this->createMock(TokenInterface::class);
$token->method('getUser')->willReturn($user);
$this->assertFalse($this->invokeVoteOnAttribute($voter, AliasVoter::DELETE, $alias, $token));
@@ -58,13 +60,23 @@ public function testVoteOnAttributeReturnsFalseIfUserDoesNotOwnAlias(): void
public function testVoteOnAttributeReturnsFalseIfTokenUserIsNotUser(): void
{
- $voter = new AliasVoter();
$alias = new Alias();
+ $voter = new AliasVoter($this->createSecurity(false));
$token = $this->createMock(TokenInterface::class);
$token->method('getUser')->willReturn(null);
$this->assertFalse($this->invokeVoteOnAttribute($voter, AliasVoter::DELETE, $alias, $token));
}
+ public function testVoteOnAttributeAllowsAdminCustomAlias(): void
+ {
+ $user = new User();
+ $alias = new Alias();
+ $alias->setUser($user); // custom (not random)
+ $voter = new AliasVoter($this->createSecurity(true));
+ $token = $this->createMock(TokenInterface::class);
+ $token->method('getUser')->willReturn($user);
+ $this->assertTrue($this->invokeVoteOnAttribute($voter, AliasVoter::DELETE, $alias, $token));
+ }
private function invokeSupports(AliasVoter $voter, string $attribute, $subject): bool
{
$ref = new \ReflectionClass($voter);
@@ -73,6 +85,18 @@ private function invokeSupports(AliasVoter $voter, string $attribute, $subject):
return $method->invoke($voter, $attribute, $subject);
}
+ private function createSecurity(bool $isAdmin): Security
+ {
+ $security = $this->createMock(Security::class);
+ $security->method('isGranted')->willReturnCallback(function (string $role) use ($isAdmin) {
+ if (!$isAdmin) {
+ return false;
+ }
+ return in_array($role, [Roles::ADMIN, Roles::DOMAIN_ADMIN], true);
+ });
+ return $security;
+ }
+
private function invokeVoteOnAttribute(AliasVoter $voter, string $attribute, $subject, TokenInterface $token): bool
{
$ref = new \ReflectionClass($voter);