Setting up the Acl Navigation in a laminas application

Hi everyone, it’s my first post on here and I’m quite new to laminas so excuse me in advance if I ask something boring.

Since it’s my first post I’ll include the full story to give as much info as possible on what I’m working on, I’ll spoiler the stuff that isn’t too relevant for the question to keep it ontopic.

What I'm working on and what I made so far.

I’ve been working for a while on a laminas project (an old zend v1 website written by someone else that I’m rebuilding from scratch)

I’ve started from the laminas skeleton application, and adapted it into what I needed.

Basically at the moment I have built the full structure of the website with the main application module and 5 modules containing each a main website feature (one for users/accounts, one for authentication and authorization and the remaining 3 for the website content)

The authentication code is working (it may need some tweaks) and properly using bcrypt.
The authorization part is handled with the acl package but I’m still struggling a bit to set it up.

I was setting up the ACL package when I noticed the existence of the mysterious AclListener, according to what I could find, if properly implemented it can handle the showing/hiding of the various navigation elements according to the ACL permisions of the user.

Sadly I’m new both to Laminas listeners and to Laminas Acl (to Laminas in general to be honest)
and all i could find is THIS documentation of the class on GitHub.

EDIT 3: Doing more resarch it seems like I don’t need to setup the AclListener and that I should instead configure the Navigation Helper with setAcl and setRole.

here is what I’ve set up so far:

Navigation config in module/application/config/module.config.php
    'navigation' => [
        'default' => [
            [
                'label' => 'Home',
                'route' => 'home',
                'resource' => 'home',
            ],
            [
                'label' => 'User management',
                'route' => 'users',
                'resource' => 'users',
                'pages' => [
                    [
                        'label'  => 'New user',
                        'route'  => 'users',
                        'action' => 'add',
                    ],
... // and so on for the other pages and sub pages
Acl config in module/Auth/config/module.config.php
'acl' => [
        'roles' => [
            "none" => [],
            "anonymous" =>["none"],
            ...
        ],
        'resources' => [
            new Resource('users'),
        	new Resource('auth'),
        	...
        ],
        'rules' => [
            [
                'type' => 'allow',
                'roles' => ['none'],
                'resources' => ['home'],
            ],
           ....
Acl and AuthenticationService factories in module/Auth/config/module.config.php
    'service_manager' => [
        'factories'=>[
            AuthenticationService::class => function($container){
                return new AuthenticationService(null,$container->get(AuthAdapter::class));            
            },
            Acl::class => function($container) {
                $config = $container->get('config');
                $aclConfig = $config['acl'] ?? [];

                $acl = new Acl();

                foreach ($aclConfig['roles'] ?? [] as $role => $parents) {
                    $acl->addRole($role,$parents);
                }

                foreach ($aclConfig['resources'] ?? [] as $resource) {
                    $acl->addResource($resource);
                }
                
                foreach ($aclConfig['rules'] ?? [] as $rule) {
                    $acl->{$rule['type']}(
                        $rule['roles'],
                        $rule['resources']
                    );
                }

                return $acl;
            },
        ]
    ],
REMOVED AclListenerand injection in module/application/src/module.php
    public function onBootstrap(MvcEvent $e)
    {
        $acl = $e->getApplication()->getServiceManager()->get(Acl::class);
        $authService = $e->getApplication()->getServiceManager()->get(AuthenticationService::class);

        $navigation = $e->getApplication()->getServiceManager()->get('ViewHelperManager')->get('Navigation');
        $aclListener = new AclListener($acl, $authService, $navigation);
        $e->getApplication()->getEventManager()->attach(MvcEvent::EVENT_ROUTE, [$aclListener, 'accept']);
    }
Navbar element in module/application/view/layout/layout.phtml
                <div class="collapse navbar-collapse" id="navbarSupportedContent">
                    <?php $this->navigation()->menu()->setPartial('partial/navmenu'); ?>
                    <?= $this->navigation('navigation')
                            ->menu()
                            ->setMinDepth()
                            ->setMaxDepth()
                            ->setUlClass('nav navbar-nav') ?>
                </div>
navmenu custom partial in module/application/view/partials/navmenu.phtml

(forgive me for the horrible code, is a temporary solution to get a decent looking navbar, I plan to come back and rewrite it once i finish the main stuff)

<ul class="nav navbar-nav">
    <?php
    foreach ($this->container as $page) {
        if($page->hasPages()){
            echo '<li class="nav-item dropdown">';
            echo '<div class="btn-group">';
            echo '<a class="nav-link';
            if($page->isActive()) echo " active";
            echo '" href="'.$page->getHref().'">'.$page->getLabel().'</a>';
            echo '<a class="nav-link dropdown-toggle dropdown-toggle-split" href="#" id="navbarDropdownMenuLink" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
            
            </a> <div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">';
            foreach ($page->pages as $subpage){
                echo '<a class="dropdown-item" href="'.$subpage->getHref().'">'.$subpage->getLabel().'</a>';
            }
            echo'</div></div></li>';  
        }else{
            echo '<li class="nav-item "><a href="'.$page->getHref().'"class="nav-link';
            if($page->isActive()) echo " active";
            echo '">'.$this->escapeHtml($page->getLabel()).'</a></li>';
        }
    }
    ?>
</ul>
View Helper configuration in module/auth/src/module.php
    public function getViewHelperConfig()
    {
        return [
            'factories' => [
                // This will overwrite the native navigation helper
                'navigation' => function(HelperPluginManager $pm) {
                    // Setup ACL:
                    $acl = $container->get(Acl::class);
                    $auth = $container->get(AuthenticationService::class);
                    // Get an instance of the proxy helper
                    $navigation = $pm->get('Laminas\View\Helper\Navigation');
                    // Store ACL and role in the proxy helper:
                    $navigation->setAcl($acl);
                    $navigation->setRole($auth->getIdentity->role);
                    // Return the new navigation helper instance
                    return $navigation;
                }
            ]
        ];
    }```

I should have posted all the relevant code, my problem is that I simply don’t understand where to go from here, It’s clear that I have to setup something with the navigation config to tell the acllistener which elements should be hidden, and I’m also quite sure that the acl configuration isn’t correct but I couldn’t find much and so I decided to post here while I look for a solution my self.

Edit, to get the role for the ACL I’m using AuthenticationService->getStorage()->write([$username, $role]) so that i can then grab it with the getIdentity() method

Edit2: I fixed the acl config

Edit3: removed the AclListener stuff and added the ViewHelperConfiguration
at this point the navigation still doesn’t hide the elements that should be hidden to a role

Hello and welcome to our forums! :smiley:

In the GitHub repository of laminas-navigation you cand find two pull requests with delegators to set the role and the ACL to view helpers:

Copy the code, use these delegators, and you are done.

And the concept of the delegators is explained in the documentation of laminas-servicemanager:

2 Likes

Thanks for the answer, I red the delegators doc and the code of the two delegators but I’m a bit confused on how to use the delegators

from what i understand first i setup the factories in a config file, in my case I added

    'navigation_helpers' => [
        'delegators' => [
            Laminas\View\Helper\Navigation::class => [
                Laminas\Navigation\View\RoleFromAuthenticationIdentityDelegator::class,
            ],        
            Laminas\View\Helper\Navigation::class => [
                Laminas\Navigation\View\PermissionAclDelegatorFactory::class,
            ],
        ],
    ],

to my Module/auth/config/module.config.php.

from what I understand the next step would be to grab the navigation element that is now encapsulated by the delegators and use the setAcl/setRole methods to set it up.

Am I correct so far?

I’m not sure about the location in which I should add this setup part though, from what i saw from other users answers/discussions an option would be to put it in Application/view/layout/layout.phtml but it feels a bit strange to bring acl and auth object in to a view code

Copy the code of the two delegators and insert it into your application module, for example. Use the correct namespace of the module for the classes:

  • Application\Navigation\RoleFromAuthenticationIdentityDelegator
  • Application\Navigation\PermissionAclDelegatorFactory

Then extend the configuration:

'navigation_helpers' => [
    'delegators' => [
        Laminas\View\Helper\Navigation::class => [
            Application\Navigation\RoleFromAuthenticationIdentityDelegator::class,
            Application\Navigation\PermissionAclDelegatorFactory::class,
        ],
    ],
],

The AuthenticationService is already registered but an alias for the Acl must be registered:

'service_manager' => [
    'factories'=>[
        // …
    ],
    'aliases' => [
        Laminas\Acl\AclInterface::class => Acl::class,
    ],
],

Otherwise the delegator will not find your Acl service.

Then nothing more is needed and the work is done by the delegators.

1 Like

I did what you suggested but I still can’t get the two delegators to load, it’s as if the navigation_helpers config is never applyed (I can see it loaded in the dev bar but it doesn’t seem to have any effect on the navigation at all (infact I can edit/delete/rename the two adapters and i get no errors)
my $identity object needs a getRole method for the delegator to hook right?

I solved the problem thanks to what @froschdesign suggested with a few modifications and some extra fixes. As soon as I have time I’ll try to document my solution as well as possible so that it can be of help to other users.

(the main problems where: namespace problems in the config ( Laminas\View\Helper\Navigation became myApp\myModule\Laminas\View\Helper\Navigation), missing acl integration in the navigation partial, error in the namespace of the acl alias Laminas\Acl\AclInterface should be Laminas\Permisions\Acl\AclInterface , a very dumb mistake I made with setting the Identity object (that caused a sort of memory leak and made me waste 4 hours looking for the cause of my session malfunction ahahah)

Anyway now I’m writing my thesis so I can’t write a full clean solution but I’ll do it asap to thank the community

1 Like

I hope you have also understood the topic delegators and can use it elsewhere as well.

1 Like