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

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

Enable the error reporting or look in the server logs.

Why factory?

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

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")

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