What is the best approach for using zend-navigation (with Acl) and AuthorizationMiddleware

authorization
middleware
zend-navigation

#1

Hy,
for one of my projects I would like to use zend-navigation with Acl and an authorization middleware. But despite all my attempts I can not do it and at the same time could you tell me if it’s the right approach.

About zend-navigation I did this:

I have this in my configuration :

'acl' => [
        'roles' => [
            'visiteur'          =>  [],
            'membre'            =>  ['visiteur'],
            'administrateur'    =>  ['membre'],
        ],
        'resources' => [
            'administrateur.action',
            'membre.action',
            'visiteur.action',
            'login.action'
        ],
        'allow' => [
            'administrateur'    => ['administrateur.action'],
            'membre'            => ['membre.action'],
            'visiteur'          => ['visiteur.action', 'login.action'],
        ],
        'deny'  =>  [
            'administrateur'    =>  ['login.action'],
            'membre'            =>  ['login.action'],
        ]
    ],
    'navigation' => [
        'menu_gauche' => [
            [
                'label'     =>  'Boite à outils',
                'route'     =>  'outils',
                'resource'  =>  'membre.action',
            ],
            [
                'label'     =>  'Administration',
                'route'     =>  'admin',
                'resource'  =>  'administrateur.action',
            ],
        ],
        'menu_droit' => [
            [
                'label' => 'Connexion',
                'route' => 'login',
                'resource'  =>  'login.action',
            ],
            [
                'label' => 'déconnexion',
                'route' => 'logout',
                'resource'  =>  'membre.action',
            ],
        ],
    ],
    'event_manager'   => [
        'lazy_listeners' => [
            [
                'listener' => Application\Listener\NavigationListener::class,
                'method'   => 'addAcl',
                'event'    => Zend\Mvc\MvcEvent::EVENT_RENDER,
                'priority' => -100,
            ],
        ],
    ],

In my Module.php :

public function getServiceConfig()
    {
        return [
            'factories' => [
                Listener\NavigationListener::class => InvokableFactory::class,

                Middleware\AuthorizationMiddleware::class  =>  ConfigAbstractFactory::class,
            ],
        ];
    }

    /**
     * {@inheritDoc}
     * @see \Zend\ModuleManager\Feature\BootstrapListenerInterface::onBootstrap()
     */
    public function onBootstrap(\Zend\EventManager\EventInterface $e)
    {
        $application = $e->getApplication();
        $eventManager = $application->getEventManager();
        $services     = $application->getServiceManager();
        $config = $application->getServiceManager()->get('config');
        
        $eventManager->attach($e::EVENT_DISPATCH, function ($e) use ($services) {
            $request  = Psr7ServerRequest::fromZend($e->getRequest());
            $response = Psr7Response::fromZend($e->getResponse());
            $done     = function ($request, $response) {
            };
            
            $authorizationMiddelware = $services->get(Middleware\AuthorizationMiddleware::class);
            
            
            $result   = $authorizationMiddelware(
                $request->withAttribute('mvcEvent', $e),
                $response,
                $done
            );
            
            if ($result) {
                return Psr7Response::toZend($result);
            }
        }, 2);

        if (array_key_exists('event_manager', $config)
            && is_array($config['event_manager'])
            && array_key_exists('lazy_listeners', $config['event_manager'])
        ) {
            $aggregate = new LazyListenerAggregate(
                $config['event_manager']['lazy_listeners'],
                $application->getServiceManager()
            );
            $aggregate->attach($application->getEventManager());
        }
    }

In my NavigationListener.php :

class NavigationListener
{
    /**
     * @param MvcEvent $event
     */
    public function addAcl(MvcEvent $event)
    {
        $serviceManager = $event->getApplication()->getServiceManager();

        /**
         * @var \Zend\View\HelperPluginManager $helperPluginManager
         */
        $helperPluginManager = $serviceManager->get('ViewHelperManager');

        /**
         * @var \Zend\View\Helper\Navigation $plugin
         */
        $plugin = $helperPluginManager->get('navigation');
        /**
         * @var Acl $acl
         */
        $acl = $serviceManager->get(AclFactory::class);

        /**
         * @var Container $session
         */
        $session = $serviceManager->get(Session::class);
        $role = ($session->offsetExists('role'))? $session->offsetGet('role') : 'visiteur';

        $plugin->setDefaultAcl($acl);
        $plugin->setRole($role);

        $this->configureMenuHelper($plugin->menu());

    }

    private function configureMenuHelper(Menu $menu)
    {
        $menu->setPartial('partials/menu');
    }

}

In my AuthorizationMiddleware.php :

class AuthorizationMiddleware
{
    /**
     * @var Acl
     */
    private $acl;

    /**
     * @var Container
     */
    private $session;

    public function __construct(
        Acl $acl,
        Container $session
    ) {
        $this->acl = $acl;
        $this->session = $session;
    }
    public function __invoke(RequestInterface $request, ResponseInterface $response, callable $next = null)
    {
        /**
         * @var MvcEvent $mvcEvent
         */
        $mvcEvent= $request->getAttribute('mvcEvent');

        $role = ($this->session->offsetExists('role'))
            ? $this->session->offsetGet('role')
            : 'visiteur';

        $routeParam = $mvcEvent->getRouteMatch()->getParams();
        $resource = (isset($routeParam['resource']))? $routeParam['resource'] : 'visiteur.action';


        if (! $this->acl->isAllowed($role, $resource)) {
            return new RedirectResponse('/login', 403);
        }
    }
}

With this code I get a blank page, and despite all my attempts I do not see how to return the correct answer from the authorization middleware,

Thank you in advance for your suggestions, advice or good practices.

PS: if you are missing code to help me to the solution do not hesitate.

cordially


#2

Enable the error reporting or look in the server logs.

Why factory?


#3

first of all thank you for your answer and take your time to help me

I just checked everything and I do not understand the reports of errors are activated both at the level of the appliation at the level of the server and no log is added …

this factory returns to me my Acl object like this:

class AclFactory implements FactoryInterface
{
    /**
     * {@inheritDoc}
     * @see \Zend\ServiceManager\Factory\FactoryInterface::__invoke()
     */
    public function __invoke(\Interop\Container\ContainerInterface $container, $requestedName, array $options = null)
    {
        // TODO Auto-generated method stub
        $config = $container->get('config');
        $config = $config['acl'];
        
        $acl = new Acl();

        $this->injectRoles($acl, $config['roles']);
        $this->injectResources($acl, $config['resources']);
        $this->injectPermissions($acl, (isset($config['allow']))? $config['allow'] : [], 'allow');
        $this->injectPermissions($acl, (isset($config['deny']))? $config['deny'] : [], 'deny');

        return $acl;
    }

    private function injectRoles(Acl $acl, array $roles)
    {
        foreach ($roles as $role => $parents) {
            foreach ($parents as $parent) {
                if (! $acl->hasRole($parent)) {
                    try {
                        $acl->addRole($parent);
                    } catch (AclExceptionInterface $e) {
                        throw new Exception\InvalidConfigException($e->getMessage(), $e->getCode(), $e);
                    }
                }
            }
            try {
                $acl->addRole($role, $parents);
            } catch (AclExceptionInterface $e) {
                throw new Exception\InvalidConfigException($e->getMessage(), $e->getCode(), $e);
            }
        }
    }

    private function injectResources(Acl $acl, array $resources)
    {
        foreach ($resources as $resource) {
            try {
                $acl->addResource($resource);
            } catch (AclExceptionInterface $e) {
                throw new Exception\InvalidConfigException($e->getMessage(), $e->getCode(), $e);
            }
        }
    }

    private function injectPermissions(Acl $acl, array $permissions, $type)
    {
        if (! in_array($type, ['allow', 'deny'], true)) {
            throw new Exception\InvalidConfigException(sprintf(
                'Invalid permission type "%s" provided in configuration; must be one of "allow" or "deny"',
                $type
            ));
        }
        foreach ($permissions as $role => $resources) {
            try {
                $acl->$type($role, $resources);
            } catch (AclExceptionInterface $e) {
                throw new Exception\InvalidConfigException($e->getMessage(), $e->getCode(), $e);
            }
        }
    }
}

the url of the blank page does not correspond to the url (’/ login’) quadn I look at the header there is the status 403 and i see the header : Location: /login.

thank you in advance


#4

factory is not a service, you should register service as a “key_service” => “TheFactoryClass” instead in the ‘service_manager’ => ‘factories’ so you can call it with $serviceManager->get("key_service")


#5

Really sorry for this oversight I wanted to remove the superfluous method getServiceConfig my module.php file and mistakenly I removed too much line.
Here is the content of my getServiceConfig method:


use Application\Factory\AclFactory as Acl;

public function getServiceConfig ()
    {
        return [
            'factories' => [
                Factory\SessionFactory::class => Factory\SessionFactory::class,

                Acl::class => Factory\AclFactory::class,

                Listener\NavigationListener::class => InvokableFactory::class,

                Middleware\AuthorizationMiddleware::class => ConfigAbstractFactory::class,

            ]
        ];
    }

just a small precision my menu is displayed correctly according to the permissions of the logged in user. Only the redirection to the page “/ login” with the status 403 does not work whereas if I remove the status 403 I am redirected to the login page.

thanks in advance