diff --git a/src/Security/Voter/AttachmentVoter.php b/src/Security/Voter/AttachmentVoter.php index c2b17053..bd7ae4df 100644 --- a/src/Security/Voter/AttachmentVoter.php +++ b/src/Security/Voter/AttachmentVoter.php @@ -41,6 +41,7 @@ use App\Entity\Attachments\UserAttachment; use RuntimeException; 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 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 @@ -65,7 +66,8 @@ final class AttachmentVoter extends Voter } 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); } - 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; diff --git a/src/Security/Voter/GroupVoter.php b/src/Security/Voter/GroupVoter.php index 34839d38..f2ce6953 100644 --- a/src/Security/Voter/GroupVoter.php +++ b/src/Security/Voter/GroupVoter.php @@ -25,6 +25,7 @@ namespace App\Security\Voter; use App\Entity\UserSystem\Group; use App\Services\UserSystem\VoterHelper; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Vote; use Symfony\Component\Security\Core\Authorization\Voter\Voter; /** @@ -43,9 +44,9 @@ final class GroupVoter extends Voter * * @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); } /** diff --git a/src/Security/Voter/ImpersonateUserVoter.php b/src/Security/Voter/ImpersonateUserVoter.php index edf55c62..1f8a70c6 100644 --- a/src/Security/Voter/ImpersonateUserVoter.php +++ b/src/Security/Voter/ImpersonateUserVoter.php @@ -26,6 +26,7 @@ namespace App\Security\Voter; use App\Entity\UserSystem\User; use App\Services\UserSystem\VoterHelper; 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\User\UserInterface; @@ -47,9 +48,16 @@ final class ImpersonateUserVoter extends Voter && $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 @@ -61,4 +69,4 @@ final class ImpersonateUserVoter extends Voter { return is_a($subjectType, User::class, true); } -} \ No newline at end of file +} diff --git a/src/Security/Voter/LabelProfileVoter.php b/src/Security/Voter/LabelProfileVoter.php index 47505bf9..cd349ddb 100644 --- a/src/Security/Voter/LabelProfileVoter.php +++ b/src/Security/Voter/LabelProfileVoter.php @@ -44,6 +44,7 @@ namespace App\Security\Voter; use App\Entity\LabelSystem\LabelProfile; use App\Services\UserSystem\VoterHelper; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Vote; use Symfony\Component\Security\Core\Authorization\Voter\Voter; /** @@ -63,9 +64,9 @@ final class LabelProfileVoter extends Voter 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 diff --git a/src/Security/Voter/LogEntryVoter.php b/src/Security/Voter/LogEntryVoter.php index 08bc3b70..dcb75a7a 100644 --- a/src/Security/Voter/LogEntryVoter.php +++ b/src/Security/Voter/LogEntryVoter.php @@ -26,6 +26,7 @@ use App\Services\UserSystem\VoterHelper; use Symfony\Bundle\SecurityBundle\Security; use App\Entity\LogSystem\AbstractLogEntry; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Vote; 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); @@ -48,19 +49,19 @@ final class LogEntryVoter extends Voter } if ('delete' === $attribute) { - return $this->helper->isGranted($token, 'system', 'delete_logs'); + return $this->helper->isGranted($token, 'system', 'delete_logs', $vote); } if ('read' === $attribute) { //Allow read of the users own log entries if ( $subject->getUser() === $user - && $this->helper->isGranted($token, 'self', 'show_logs') + && $this->helper->isGranted($token, 'self', 'show_logs', $vote) ) { return true; } - return $this->helper->isGranted($token, 'system', 'show_logs'); + return $this->helper->isGranted($token, 'system', 'show_logs', $vote); } if ('show_details' === $attribute) { diff --git a/src/Security/Voter/OrderdetailVoter.php b/src/Security/Voter/OrderdetailVoter.php index 20843b9a..3bb2a3a3 100644 --- a/src/Security/Voter/OrderdetailVoter.php +++ b/src/Security/Voter/OrderdetailVoter.php @@ -46,6 +46,7 @@ use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Parts\Part; use App\Entity\PriceInformations\Orderdetail; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Vote; 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 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)) { 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 (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 diff --git a/src/Security/Voter/ParameterVoter.php b/src/Security/Voter/ParameterVoter.php index 8ee2b9f5..f59bdeaf 100644 --- a/src/Security/Voter/ParameterVoter.php +++ b/src/Security/Voter/ParameterVoter.php @@ -39,6 +39,7 @@ use App\Entity\Parameters\StorageLocationParameter; use App\Entity\Parameters\SupplierParameter; use RuntimeException; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Vote; 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; @@ -108,7 +109,7 @@ final class ParameterVoter extends Voter 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 diff --git a/src/Security/Voter/PartAssociationVoter.php b/src/Security/Voter/PartAssociationVoter.php index 7678b67a..f1eb83c7 100644 --- a/src/Security/Voter/PartAssociationVoter.php +++ b/src/Security/Voter/PartAssociationVoter.php @@ -46,6 +46,7 @@ use App\Services\UserSystem\VoterHelper; use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Parts\Part; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Vote; 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 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) { 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 (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 diff --git a/src/Security/Voter/PartLotVoter.php b/src/Security/Voter/PartLotVoter.php index a64473c8..87c3d135 100644 --- a/src/Security/Voter/PartLotVoter.php +++ b/src/Security/Voter/PartLotVoter.php @@ -46,6 +46,7 @@ use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Vote; 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 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); 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; //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(); } + if (!$lot_permission) { + $vote->addReason('User is not the owner of the lot.'); + } + 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 (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 diff --git a/src/Security/Voter/PartVoter.php b/src/Security/Voter/PartVoter.php index ef70b6ce..159e6893 100644 --- a/src/Security/Voter/PartVoter.php +++ b/src/Security/Voter/PartVoter.php @@ -25,6 +25,7 @@ namespace App\Security\Voter; use App\Entity\Parts\Part; use App\Services\UserSystem\VoterHelper; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Vote; use Symfony\Component\Security\Core\Authorization\Voter\Voter; /** @@ -52,10 +53,9 @@ final class PartVoter extends Voter 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); + return $this->helper->isGranted($token, 'parts', $attribute, $vote); } public function supportsAttribute(string $attribute): bool diff --git a/src/Security/Voter/PricedetailVoter.php b/src/Security/Voter/PricedetailVoter.php index 681b73b7..ca86f1ce 100644 --- a/src/Security/Voter/PricedetailVoter.php +++ b/src/Security/Voter/PricedetailVoter.php @@ -47,6 +47,7 @@ use App\Entity\PriceInformations\Orderdetail; use App\Entity\Parts\Part; use App\Entity\PriceInformations\Pricedetail; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Vote; 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 function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool + protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token, ?Vote $vote = null): bool { $operation = match ($attribute) { 'read' => 'read', @@ -72,7 +73,7 @@ final class PricedetailVoter extends Voter //If we have no part associated use the generic part permission 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 diff --git a/src/Security/Voter/StructureVoter.php b/src/Security/Voter/StructureVoter.php index 2417b796..ad0299a7 100644 --- a/src/Security/Voter/StructureVoter.php +++ b/src/Security/Voter/StructureVoter.php @@ -33,6 +33,7 @@ use App\Entity\Parts\Supplier; use App\Entity\PriceInformations\Currency; use App\Services\UserSystem\VoterHelper; 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 function is_object; @@ -113,10 +114,10 @@ final class StructureVoter extends Voter * * @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); //Just resolve the permission - return $this->helper->isGranted($token, $permission_name, $attribute); + return $this->helper->isGranted($token, $permission_name, $attribute, $vote); } } diff --git a/src/Security/Voter/UserVoter.php b/src/Security/Voter/UserVoter.php index b41c1a40..97f8e4fb 100644 --- a/src/Security/Voter/UserVoter.php +++ b/src/Security/Voter/UserVoter.php @@ -26,6 +26,7 @@ use App\Entity\UserSystem\User; use App\Services\UserSystem\PermissionManager; use App\Services\UserSystem\VoterHelper; 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 function in_array; @@ -79,7 +80,7 @@ final class UserVoter extends Voter * * @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); @@ -97,7 +98,7 @@ final class UserVoter extends Voter if (($subject instanceof User) && $subject->getID() === $user->getID() && $this->helper->isValidOperation('self', $attribute)) { //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: if ($tmp) { return $tmp; @@ -106,7 +107,7 @@ final class UserVoter extends Voter //Else just check user permission: if ($this->helper->isValidOperation('users', $attribute)) { - return $this->helper->isGranted($token, 'users', $attribute); + return $this->helper->isGranted($token, 'users', $attribute, $vote); } return false; diff --git a/src/Services/UserSystem/VoterHelper.php b/src/Services/UserSystem/VoterHelper.php index dda00de7..bf65c58c 100644 --- a/src/Services/UserSystem/VoterHelper.php +++ b/src/Services/UserSystem/VoterHelper.php @@ -54,11 +54,16 @@ final class VoterHelper * @param TokenInterface $token The token to check * @param string $permission The permission 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 */ - 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; } /**