0
votes

I have a website made with Symfony 3.4 and within my actions I must check if the current user can edit the target product, something like this:

/**
 * @Route("/products/{id}/edit")
 */
public function editAction(Request $request, Product $product)
{
    // security
    $user = $this->getUser();
    if ($user != $product->getUser()) {
        throw $this->createAccessDeniedException();
    }

    // ...
}

How can I avoid making the same check on every action (bonus points if using annotations and expressions)?

I am already using security.yml with access_control to deny access based on roles.

2
From the docs - ehymel
You can do it with a controller listener but ask yourself how much magic do you really want to introduce just to save one line of code per action. - Cerad
/products/{id}/edit is a bad design. Your method should tell it to edit not URL. e.g. PATCH - BentCoder

2 Answers

2
votes

You can use Voters for this exact purpose. No magic involved. After creating and registering the Voter authentication will be done automatically in the security layer.

You just have to create the Voter class and then register it as a service. But if you're using the default services.yaml configuration, registering it as a service is done automatically for you!

Here is an example you can use. You may have to change a few items but this is basically it.

To read more visit: https://symfony.com/doc/current/security/voters.html

<?php
namespace AppBundle\Security;

use AppBundle\Entity\Product;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use AppBundle\Entity\User;

class ProductVoter extends Voter
{
    const EDIT = 'EDIT_USER_PRODUCT';

    protected function supports($attribute, $subject)
    {
        if($attribute !== self::EDIT) {
            return false;
        }
        if(!$subject instanceof Product) {
            return false;
        }
        return true;
    }

    protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
    {
        /** @var Product $product */
        $product= $subject;

        $user = $token->getUser();

        if (!$user instanceof User) {
            // the user must be logged in; if not, deny access
            return false;
        }

        return $this->belongsToUser($product, $user);
    }

    private function belongsToUser(Product $product, User $user)
    {
        return $user->getId() === $product->getUser()->getId();
    }
}
1
votes

You could try with a listener:

  1. Check the action name,for example, if it is "edit_product", them continue.
  2. Get the current logged User.
  3. Get the user of the product entity.
  4. Check if current user is different to Product user, if it is true, throw CreateAccessDeniedException.

services.yml

app.user.listener:
    class: AppBundle\EventListener\ValidateUserListener        
    tags:
        - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
    arguments: ["@service_container", "@doctrine.orm.entity_manager"]

Edit Action:

Added name "edit_product" to the action.

/**
* 
* @Route("/products/{id}/edit",name="edit_product")
*/
public function editAction()
{
 ...

src\AppBundle\EventListener\ValidateUserListener.php

<?php

namespace AppBundle\EventListener;

use Symfony\Component\HttpKernel\Event\GetResponseEvent;

class  ValidateUserListener
{   
  private $container;
  private $entityManager;

 public function __construct($container, $entityManager)
 {     
    $this->container = $container;
    $this->entityManager = $entityManager;
 }

 public function onKernelRequest(GetResponseEvent $event)
 {
    $currentRoute = $event->getRequest()->attributes->get('_route');
    if($currentRoute=='edit_product' || $currentRoute=='edit_item' )
    {
        $array_user = $this->getCurrentUser(); 
        if($array_user['is_auth'])
        {
            $current_user = $array_user['current_user'];   

            $product = $this->entityManager->getRepository('AppBundle:User')->findOneByUsername($current_user);

            $product_user = $product->getUsername();

            if ($current_user !==$product_user)
            {
                throw $this->createAccessDeniedException();
            }
        }            
    }        
  }

private function getCurrentUser() 
{
    //Get the current logged User
    $user  =  $this->container->get('security.token_storage')->getToken()->getUser();
    if(null!=$user)
    {
        //If user is authenticated
        $isauth = $this->container->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY');
        return  array('is_auth'=>$isauth, 'current_user'=>$user);
    }
    return  array('is_auth'=>false, 'current_user'=>$user);    
   }
 }

Tested in Symfony 3.3