@Security annotation on controller class being ove

2019-08-02 06:23发布

问题:

I have a Symfony2 controller as follows:

/**
 * @Security("is_granted('my_permission')")
 */
class MyController extends Controller 
{
    /**
     * @Security("is_granted('another_permission')")
     */
    public function myAction() 
    {
        // ...
    }
}

It appears the @Security annotation on the myAction() method overrides/ignores the parent @Security annotation on the MyController class. Is there any way to make these stack, to avoid having to do:

/**
 * @Security("is_granted('my_permission') and is_granted('another_permission')")
 */
public function myAction() 
{
    // ...
}

on every action method in the controller?

回答1:

It appears the @Security annotation on the myAction method overrides/ignores the parent @Security annotation on the MyController class.

Indeed, Sensio\Bundle\FrameworkExtraBundle\Configuration\Security annotation doesn't allows nested configuration (see allowArray() method). So method configuration overrides class configuration for @Security annotation.

Is there any way to make these stack...

Not in a simple way, you need create three class and one trick to not reimplement the whole parent code:

Security.php

namespace AppBundle\Configuration;

/**
 * @Annotation
 */
class Security extends \Sensio\Bundle\FrameworkExtraBundle\Configuration\Security
{
    public function getAliasName()
    {
        return 'app_security';
    }

    public function allowArray()
    {
        // allow nested configuration (class/method).
        return true;
    }
}

SecurityConfiguration.php

This class allow you compound the final security expression through all security configurations (class/method).

namespace AppBundle\Configuration;

class SecurityConfiguration
{
    /**
     * @var Security[]
     */
    private $configurations;

    public function __construct(array $configurations)
    {
        $this->configurations = $configurations;
    }

    public function getExpression()
    {
        $expressions = [];
        foreach ($this->configurations as $configuration) {
            $expressions[] = $configuration->getExpression();
        }

        return implode(' and ', $expressions);
    }
}

SecurityListener.php

namespace AppBundle\EventListener;

use AppBundle\Configuration\SecurityConfiguration;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\HttpKernel\KernelEvents;

class SecurityListener extends \Sensio\Bundle\FrameworkExtraBundle\EventListener\SecurityListener
{
    public function onKernelController(FilterControllerEvent $event)
    {
        $request = $event->getRequest();
        if (!$configuration = $request->attributes->get('_app_security')) {
            return;
        }

        // trick to simulate one security configuration (all in one class/method).
        $request->attributes->set('_security', new SecurityConfiguration($configuration));

        parent::onKernelController($event);
    }

    public static function getSubscribedEvents()
    {
        // this listener must be called after Sensio\Bundle\FrameworkExtraBundle\EventListener\ControllerListener.
        return array(KernelEvents::CONTROLLER => array('onKernelController', -1));
    }
}

services.yml

services:
    app.security.listener:
        class: AppBundle\EventListener\SecurityListener
        parent: sensio_framework_extra.security.listener
        tags:
            - { name: kernel.event_subscriber }

Finally, just use your @AppBundle\Configuration\Security annotation instead the standard one.



回答2:

Here's my try:

Using, in app/config/security.yml, this role hierarchy:

role_hierarchy:
    ROLE_CLASS: ROLE_CLASS
    ROLE_METHOD: [ROLE_CLASS, ROLE_METHOD]

And if I have two users: user1 with ROLE_CLASS, and user2 with ROLE_METHOD (which means this user has both roles), then the first user can see all the pages created inside the controller, except the ones that have additional restrictions.

Controller example:

/**
 * @Security("is_granted('ROLE_CLASS')")
 */
class SomeController extends Controller
{
    /**
     * @Route("/page1", name="page1")
     * @Security("is_granted('ROLE_METHOD')")
     */
    public function page1()
    {
        return $this->render('default/page1.html.twig');
    }

    /**
     * @Route("/page2", name="page2")
     */
    public function page2()
    {
        return $this->render('default/page2.html.twig');
    }
}

So because user1 has ROLE_CLASS, he is able to see just /page2, but not /page1, as he will receive a 403 Expression "is_granted('ROLE_METHOD')" denied access. error (for dev obviously). On the other hand, user2, having ROLE_METHOD (and ROLE_CLASS), he is able to see both pages.