Refactor some calls on each Zf2 controller action

142 views Asked by At

I need to do a custom isGranted method (not using Rbac or acl module from community). So I have a service which provides the functionality. But this code:

if (!$this->userService->isGrantedCustom($this->session->offsetGet('cod_lvl'), 'ZF_INV_HOM')) {
    throw new \Exception("you_are_not_allowed", 1);
}

...is duplicated in each controller and each action I have. Parameters are changing of course depends on the permission ('ZF_INV_HOM', 'ZF_TODO_DELETE' ...).

I think it's not a bad idea to do this code before the controller is called, but I can't figure what is the best solution (best architecture), and how to pass those parameters to it (I thought about annotation on controllers but how to handle this ?).

The point is, if I have to modify this code I can't imagine to do that hundreds of times, for each controllers, each action I have I need to have this code in one place.

2

There are 2 answers

2
Wilt On BEST ANSWER

If you don't want to pollute your Module with all this code you can also make a listener class and attach only the listener in your bootstrap method:

<?php

namespace Application\Listener;

use Application\Service\UserService;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\Mvc\MvcEvent;
use Zend\EventManager\SharedEventManagerInterface;
use Zend\EventManager\SharedListenerAggregateInterface;
use Zend\Authentication\AuthenticationServiceInterface;

class IsAllowedListener implements SharedListenerAggregateInterface
{
    /**
     * @var AuthenticationServiceInterface
     */
    protected $authService;

    /**
     * @var UserService
     */
    protected $userService;

    /**
     * @var \Zend\Stdlib\CallbackHandler[]
     */
    protected $sharedListeners = array();

    /**
     * @param SharedEventManagerInterface $events
     */
    public function attachShared(SharedEventManagerInterface $events)
    {
        $this->sharedListeners[] = $events->attach(AbstractActionController::class, MvcEvent::EVENT_DISPATCH, array($this, 'isAllowed'), 1000);
    }

    public function __construct(AuthenticationServiceInterface $authService, UserService $userService ){
        $this->authService = $authService;
        $this->userService = $userService;
    }

    /**
     * @param MvcEvent $event
     */
    protected function isAllowed(MvcEvent $event)
    {
        $authService = $this->getAuthService();
        $identity = $authService->getIdentity();

        $userService = $this->getUserService();

        if($userService->isGrantedCustom()){
            // User is granted we can return
            return;
        }

        // Return not allowed response
    }

    /**
     * @return AuthenticationServiceInterface
     */
    public function getAuthService()
    {
        return $this->authService;
    }

    /**
     * @param AuthenticationServiceInterface $authService
     */
    public function setAuthService(AuthenticationServiceInterface $authService)
    {
        $this->authService = $authService;
    }

    /**
     * @return UserService
     */
    public function getUserService()
    {
        return $this->userService;
    }

    /**
     * @param UserService $userService
     */
    public function setUserService(AuthenticationServiceInterface $userService)
    {
        $this->userService = $userService;
    }
}

You need to setup a factory to inject your dependencies:

<?php

namespace Application\Listener;

use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;

/**
 * Factory for creating the IsAllowedListener
 */
class IsAllowedListenerFactory implements FactoryInterface
{
    /**
     * Create the IsAllowedListener
     *
     * @param ServiceLocatorInterface $serviceLocator
     * @return RenderLinksListener
     */
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        $authService = $serviceManager->get('Zend\Authentication\AuthenticationService');
        $userService = $serviceLocator->get('Application\Service\UserService');
        return new IsAllowedListener($authService, $userService );
    }
}

And register all this in config:

'service_manager' => array(
    'factories' => array(
        'Application\Listener\IsAllowedListener' => 'Application\Listener\IsAllowedListenerFactory'
    )
)

And then in bootstrap:

public function onBootstrap(EventInterface $event)
{
    $application    = $event->getTarget();
    $serviceManager = $application->getServiceManager();
    $eventManager   = $application->getEventManager();
    $sharedEventManager = $eventManager->getSharedManager();
    $isAllowedListener = $serviceManager->get('Application\Listener\IsAllowedListener')
    $sharedEventManager->attachAggregate($isAllowedListener);
}

Instead of using AbstractActionController::class, you could also make a specific class, so you will only listen to instances of that class.

So for example AbstractIsAllowedActionController::class or something like that.

4
AlexP On

By attaching an event listener to the SharedEventManager you can target all controllers and have the authorization check in just one place.

In this case the target is Zend\Mvc\Controller\AbstractActionController which means any controller extending it will execute the listener. The high priority of this listener will mean that it is executed prior to the target controller action, giving you the chance to handle any requests that have not been authorized.

public function onBootstrap(MvcEvent $event)
{
    $application  = $event->getApplication();
    $eventManager = $application->getEventManager()->getSharedManager();

    $eventManager->attach(
        \Zend\Mvc\Controller\AbstractActionController::class, // Identity of the target controller
        MvcEvent::EVENT_DISPATCH,
        [$this, 'isAllowed'],
        1000  // high priority
    );
}

In each controller there would need to be some way that you can determine which 'resource' is being accessed.

As an example it could implement this interface

interface ResourceInterface
{
    // Return a unique key representing the resource
    public function getResourceId();
}

The listener could then look like this.

public function isAllowed(MvcEvent $event)
{
    $serviceManager = $event->getApplication()->getServiceManager();

    // We need the 'current' user identity
    $authService = $serviceManager->get('Zend\Authentication\AuthenticationService');
    $identity = $authService->getIdentity();

    // The service that performs the authorization
    $userService = $serviceManager->get('MyModule\Service\UserService');

    // The target controller is itself a resource (the thing we want to access)
    // in this example it returns an resource id so we know what we want to access
    // but you could also get this 'id' from the request or config etc
    $controller = $event->getTarget();

    if ($controller instanceof ResourceInterface) {
        $resourceName = $controller->getResourceId();

        // Test the authorization, is UserX allowed resource ID Y
        if (empty($resourceName) || $userService->isGrantedCustom($identity, $resourceName)) {
            // early exit for success
            return;
        } else {
           // Denied; perhaps trigger a new custom event or return a response
        }
    }

}