Added voter reason explaination to the other voters

This commit is contained in:
Jan Böhmer 2025-09-06 00:24:55 +02:00
parent 117ff4484d
commit eb4258053e
14 changed files with 65 additions and 35 deletions

View file

@ -41,6 +41,7 @@ use App\Entity\Attachments\UserAttachment;
use RuntimeException; use RuntimeException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use function in_array; use function in_array;
@ -56,7 +57,7 @@ final class AttachmentVoter extends Voter
{ {
} }
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token, ?Vote $vote = null): bool
{ {
//This voter only works for attachments //This voter only works for attachments
@ -65,7 +66,8 @@ final class AttachmentVoter extends Voter
} }
if ($attribute === 'show_private') { if ($attribute === 'show_private') {
return $this->helper->isGranted($token, 'attachments', 'show_private'); $vote?->addReason('User is not allowed to view private attachments.');
return $this->helper->isGranted($token, 'attachments', 'show_private', $vote);
} }
@ -111,7 +113,8 @@ final class AttachmentVoter extends Voter
throw new RuntimeException('Encountered unknown Parameter type: ' . $subject); throw new RuntimeException('Encountered unknown Parameter type: ' . $subject);
} }
return $this->helper->isGranted($token, $param, $this->mapOperation($attribute)); $vote?->addReason('User is not allowed to '.$this->mapOperation($attribute).' attachments of type '.$param.'.');
return $this->helper->isGranted($token, $param, $this->mapOperation($attribute), $vote);
} }
return false; return false;

View file

@ -25,6 +25,7 @@ namespace App\Security\Voter;
use App\Entity\UserSystem\Group; use App\Entity\UserSystem\Group;
use App\Services\UserSystem\VoterHelper; use App\Services\UserSystem\VoterHelper;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Authorization\Voter\Voter;
/** /**
@ -43,9 +44,9 @@ final class GroupVoter extends Voter
* *
* @param string $attribute * @param string $attribute
*/ */
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token, ?Vote $vote = null): bool
{ {
return $this->helper->isGranted($token, 'groups', $attribute); return $this->helper->isGranted($token, 'groups', $attribute, $vote);
} }
/** /**

View file

@ -26,6 +26,7 @@ namespace App\Security\Voter;
use App\Entity\UserSystem\User; use App\Entity\UserSystem\User;
use App\Services\UserSystem\VoterHelper; use App\Services\UserSystem\VoterHelper;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserInterface;
@ -47,9 +48,16 @@ final class ImpersonateUserVoter extends Voter
&& $subject instanceof UserInterface; && $subject instanceof UserInterface;
} }
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool
{ {
return $this->helper->isGranted($token, 'users', 'impersonate'); $result = $this->helper->isGranted($token, 'users', 'impersonate');
if ($result === false) {
$vote?->addReason('User is not allowed to impersonate other users.');
$this->helper->addReason($vote, 'users', 'impersonate');
}
return $result;
} }
public function supportsAttribute(string $attribute): bool public function supportsAttribute(string $attribute): bool

View file

@ -44,6 +44,7 @@ namespace App\Security\Voter;
use App\Entity\LabelSystem\LabelProfile; use App\Entity\LabelSystem\LabelProfile;
use App\Services\UserSystem\VoterHelper; use App\Services\UserSystem\VoterHelper;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Authorization\Voter\Voter;
/** /**
@ -63,9 +64,9 @@ final class LabelProfileVoter extends Voter
public function __construct(private readonly VoterHelper $helper) public function __construct(private readonly VoterHelper $helper)
{} {}
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token, ?Vote $vote = null): bool
{ {
return $this->helper->isGranted($token, 'labels', self::MAPPING[$attribute]); return $this->helper->isGranted($token, 'labels', self::MAPPING[$attribute], $vote);
} }
protected function supports($attribute, $subject): bool protected function supports($attribute, $subject): bool

View file

@ -26,6 +26,7 @@ use App\Services\UserSystem\VoterHelper;
use Symfony\Bundle\SecurityBundle\Security; use Symfony\Bundle\SecurityBundle\Security;
use App\Entity\LogSystem\AbstractLogEntry; use App\Entity\LogSystem\AbstractLogEntry;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Authorization\Voter\Voter;
/** /**
@ -39,7 +40,7 @@ final class LogEntryVoter extends Voter
{ {
} }
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token, ?Vote $vote = null): bool
{ {
$user = $this->helper->resolveUser($token); $user = $this->helper->resolveUser($token);
@ -48,19 +49,19 @@ final class LogEntryVoter extends Voter
} }
if ('delete' === $attribute) { if ('delete' === $attribute) {
return $this->helper->isGranted($token, 'system', 'delete_logs'); return $this->helper->isGranted($token, 'system', 'delete_logs', $vote);
} }
if ('read' === $attribute) { if ('read' === $attribute) {
//Allow read of the users own log entries //Allow read of the users own log entries
if ( if (
$subject->getUser() === $user $subject->getUser() === $user
&& $this->helper->isGranted($token, 'self', 'show_logs') && $this->helper->isGranted($token, 'self', 'show_logs', $vote)
) { ) {
return true; return true;
} }
return $this->helper->isGranted($token, 'system', 'show_logs'); return $this->helper->isGranted($token, 'system', 'show_logs', $vote);
} }
if ('show_details' === $attribute) { if ('show_details' === $attribute) {

View file

@ -46,6 +46,7 @@ use Symfony\Bundle\SecurityBundle\Security;
use App\Entity\Parts\Part; use App\Entity\Parts\Part;
use App\Entity\PriceInformations\Orderdetail; use App\Entity\PriceInformations\Orderdetail;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Authorization\Voter\Voter;
/** /**
@ -59,7 +60,7 @@ final class OrderdetailVoter extends Voter
protected const ALLOWED_PERMS = ['read', 'edit', 'create', 'delete', 'show_history', 'revert_element']; protected const ALLOWED_PERMS = ['read', 'edit', 'create', 'delete', 'show_history', 'revert_element'];
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token, ?Vote $vote = null): bool
{ {
if (! is_a($subject, Orderdetail::class, true)) { if (! is_a($subject, Orderdetail::class, true)) {
throw new \RuntimeException('This voter can only handle Orderdetail objects!'); throw new \RuntimeException('This voter can only handle Orderdetail objects!');
@ -75,7 +76,7 @@ final class OrderdetailVoter extends Voter
//If we have no part associated use the generic part permission //If we have no part associated use the generic part permission
if (is_string($subject) || !$subject->getPart() instanceof Part) { if (is_string($subject) || !$subject->getPart() instanceof Part) {
return $this->helper->isGranted($token, 'parts', $operation); return $this->helper->isGranted($token, 'parts', $operation, $vote);
} }
//Otherwise vote on the part //Otherwise vote on the part

View file

@ -39,6 +39,7 @@ use App\Entity\Parameters\StorageLocationParameter;
use App\Entity\Parameters\SupplierParameter; use App\Entity\Parameters\SupplierParameter;
use RuntimeException; use RuntimeException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Authorization\Voter\Voter;
/** /**
@ -53,7 +54,7 @@ final class ParameterVoter extends Voter
{ {
} }
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token, ?Vote $vote = null): bool
{ {
//return $this->resolver->inherit($user, 'attachments', $attribute) ?? false; //return $this->resolver->inherit($user, 'attachments', $attribute) ?? false;
@ -108,7 +109,7 @@ final class ParameterVoter extends Voter
throw new RuntimeException('Encountered unknown Parameter type: ' . (is_object($subject) ? $subject::class : $subject)); throw new RuntimeException('Encountered unknown Parameter type: ' . (is_object($subject) ? $subject::class : $subject));
} }
return $this->helper->isGranted($token, $param, $attribute); return $this->helper->isGranted($token, $param, $attribute, $vote);
} }
protected function supports(string $attribute, $subject): bool protected function supports(string $attribute, $subject): bool

View file

@ -46,6 +46,7 @@ use App\Services\UserSystem\VoterHelper;
use Symfony\Bundle\SecurityBundle\Security; use Symfony\Bundle\SecurityBundle\Security;
use App\Entity\Parts\Part; use App\Entity\Parts\Part;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Authorization\Voter\Voter;
/** /**
@ -61,7 +62,7 @@ final class PartAssociationVoter extends Voter
protected const ALLOWED_PERMS = ['read', 'edit', 'create', 'delete', 'show_history', 'revert_element']; protected const ALLOWED_PERMS = ['read', 'edit', 'create', 'delete', 'show_history', 'revert_element'];
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token, ?Vote $vote = null): bool
{ {
if (!is_string($subject) && !$subject instanceof PartAssociation) { if (!is_string($subject) && !$subject instanceof PartAssociation) {
throw new \RuntimeException('Invalid subject type!'); throw new \RuntimeException('Invalid subject type!');
@ -77,7 +78,7 @@ final class PartAssociationVoter extends Voter
//If we have no part associated use the generic part permission //If we have no part associated use the generic part permission
if (is_string($subject) || !$subject->getOwner() instanceof Part) { if (is_string($subject) || !$subject->getOwner() instanceof Part) {
return $this->helper->isGranted($token, 'parts', $operation); return $this->helper->isGranted($token, 'parts', $operation, $vote);
} }
//Otherwise vote on the part //Otherwise vote on the part

View file

@ -46,6 +46,7 @@ use Symfony\Bundle\SecurityBundle\Security;
use App\Entity\Parts\Part; use App\Entity\Parts\Part;
use App\Entity\Parts\PartLot; use App\Entity\Parts\PartLot;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Authorization\Voter\Voter;
/** /**
@ -59,13 +60,13 @@ final class PartLotVoter extends Voter
protected const ALLOWED_PERMS = ['read', 'edit', 'create', 'delete', 'show_history', 'revert_element', 'withdraw', 'add', 'move']; protected const ALLOWED_PERMS = ['read', 'edit', 'create', 'delete', 'show_history', 'revert_element', 'withdraw', 'add', 'move'];
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token, ?Vote $vote = null): bool
{ {
$user = $this->helper->resolveUser($token); $user = $this->helper->resolveUser($token);
if (in_array($attribute, ['withdraw', 'add', 'move'], true)) if (in_array($attribute, ['withdraw', 'add', 'move'], true))
{ {
$base_permission = $this->helper->isGranted($token, 'parts_stock', $attribute); $base_permission = $this->helper->isGranted($token, 'parts_stock', $attribute, $vote);
$lot_permission = true; $lot_permission = true;
//If the lot has an owner, we need to check if the user is the owner of the lot to be allowed to withdraw it. //If the lot has an owner, we need to check if the user is the owner of the lot to be allowed to withdraw it.
@ -73,6 +74,10 @@ final class PartLotVoter extends Voter
$lot_permission = $subject->getOwner() === $user || $subject->getOwner()->getID() === $user->getID(); $lot_permission = $subject->getOwner() === $user || $subject->getOwner()->getID() === $user->getID();
} }
if (!$lot_permission) {
$vote->addReason('User is not the owner of the lot.');
}
return $base_permission && $lot_permission; return $base_permission && $lot_permission;
} }
@ -86,7 +91,7 @@ final class PartLotVoter extends Voter
//If we have no part associated use the generic part permission //If we have no part associated use the generic part permission
if (is_string($subject) || !$subject->getPart() instanceof Part) { if (is_string($subject) || !$subject->getPart() instanceof Part) {
return $this->helper->isGranted($token, 'parts', $operation); return $this->helper->isGranted($token, 'parts', $operation, $vote);
} }
//Otherwise vote on the part //Otherwise vote on the part

View file

@ -25,6 +25,7 @@ namespace App\Security\Voter;
use App\Entity\Parts\Part; use App\Entity\Parts\Part;
use App\Services\UserSystem\VoterHelper; use App\Services\UserSystem\VoterHelper;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Authorization\Voter\Voter;
/** /**
@ -52,10 +53,9 @@ final class PartVoter extends Voter
return false; return false;
} }
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token, ?Vote $vote = null): bool
{ {
//Null concealing operator means, that no return $this->helper->isGranted($token, 'parts', $attribute, $vote);
return $this->helper->isGranted($token, 'parts', $attribute);
} }
public function supportsAttribute(string $attribute): bool public function supportsAttribute(string $attribute): bool

View file

@ -47,6 +47,7 @@ use App\Entity\PriceInformations\Orderdetail;
use App\Entity\Parts\Part; use App\Entity\Parts\Part;
use App\Entity\PriceInformations\Pricedetail; use App\Entity\PriceInformations\Pricedetail;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Authorization\Voter\Voter;
/** /**
@ -60,7 +61,7 @@ final class PricedetailVoter extends Voter
protected const ALLOWED_PERMS = ['read', 'edit', 'create', 'delete', 'show_history', 'revert_element']; protected const ALLOWED_PERMS = ['read', 'edit', 'create', 'delete', 'show_history', 'revert_element'];
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token, ?Vote $vote = null): bool
{ {
$operation = match ($attribute) { $operation = match ($attribute) {
'read' => 'read', 'read' => 'read',
@ -72,7 +73,7 @@ final class PricedetailVoter extends Voter
//If we have no part associated use the generic part permission //If we have no part associated use the generic part permission
if (is_string($subject) || !$subject->getOrderdetail() instanceof Orderdetail || !$subject->getOrderdetail()->getPart() instanceof Part) { if (is_string($subject) || !$subject->getOrderdetail() instanceof Orderdetail || !$subject->getOrderdetail()->getPart() instanceof Part) {
return $this->helper->isGranted($token, 'parts', $operation); return $this->helper->isGranted($token, 'parts', $operation, $vote);
} }
//Otherwise vote on the part //Otherwise vote on the part

View file

@ -33,6 +33,7 @@ use App\Entity\Parts\Supplier;
use App\Entity\PriceInformations\Currency; use App\Entity\PriceInformations\Currency;
use App\Services\UserSystem\VoterHelper; use App\Services\UserSystem\VoterHelper;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use function is_object; use function is_object;
@ -113,10 +114,10 @@ final class StructureVoter extends Voter
* *
* @param string $attribute * @param string $attribute
*/ */
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token, ?Vote $vote = null): bool
{ {
$permission_name = $this->instanceToPermissionName($subject); $permission_name = $this->instanceToPermissionName($subject);
//Just resolve the permission //Just resolve the permission
return $this->helper->isGranted($token, $permission_name, $attribute); return $this->helper->isGranted($token, $permission_name, $attribute, $vote);
} }
} }

View file

@ -26,6 +26,7 @@ use App\Entity\UserSystem\User;
use App\Services\UserSystem\PermissionManager; use App\Services\UserSystem\PermissionManager;
use App\Services\UserSystem\VoterHelper; use App\Services\UserSystem\VoterHelper;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use function in_array; use function in_array;
@ -79,7 +80,7 @@ final class UserVoter extends Voter
* *
* @param string $attribute * @param string $attribute
*/ */
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token, ?Vote $vote = null): bool
{ {
$user = $this->helper->resolveUser($token); $user = $this->helper->resolveUser($token);
@ -97,7 +98,7 @@ final class UserVoter extends Voter
if (($subject instanceof User) && $subject->getID() === $user->getID() && if (($subject instanceof User) && $subject->getID() === $user->getID() &&
$this->helper->isValidOperation('self', $attribute)) { $this->helper->isValidOperation('self', $attribute)) {
//Then we also need to check the self permission //Then we also need to check the self permission
$tmp = $this->helper->isGranted($token, 'self', $attribute); $tmp = $this->helper->isGranted($token, 'self', $attribute, $vote);
//But if the self value is not allowed then use just the user value: //But if the self value is not allowed then use just the user value:
if ($tmp) { if ($tmp) {
return $tmp; return $tmp;
@ -106,7 +107,7 @@ final class UserVoter extends Voter
//Else just check user permission: //Else just check user permission:
if ($this->helper->isValidOperation('users', $attribute)) { if ($this->helper->isValidOperation('users', $attribute)) {
return $this->helper->isGranted($token, 'users', $attribute); return $this->helper->isGranted($token, 'users', $attribute, $vote);
} }
return false; return false;

View file

@ -54,11 +54,16 @@ final class VoterHelper
* @param TokenInterface $token The token to check * @param TokenInterface $token The token to check
* @param string $permission The permission to check * @param string $permission The permission to check
* @param string $operation The operation to check * @param string $operation The operation to check
* @param Vote|null $vote The vote object to add reasons to (optional). If null, no reasons are added.
* @return bool * @return bool
*/ */
public function isGranted(TokenInterface $token, string $permission, string $operation): bool public function isGranted(TokenInterface $token, string $permission, string $operation, ?Vote $vote = null): bool
{ {
return $this->isGrantedTrinary($token, $permission, $operation) ?? false; $tmp = $this->isGrantedTrinary($token, $permission, $operation) ?? false;
if ($tmp === false) {
$this->addReason($vote, $permission, $operation);
}
return $tmp;
} }
/** /**