Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Access control

Authentication answers "who is this", and access control answers "what are they allowed to do". The most common task is keeping guests away from the pages that add and delete posts. Checking this in every action is inconvenient, so the check is placed in one spot that every request passes through.

Where to put the check

The module class has an onBootstrap() method: Laminas calls it on startup. Inside it you can subscribe to an event — a step of request handling. The MvcEvent::EVENT_ROUTE event happens when the route is already known but the controller has not been called yet. A separate lesson covers events in more detail.

Protected routes

Open module/Blog/src/Module.php and add the method (if it already exists after the sessions lesson, extend it):

// module/Blog/src/Module.php
use Laminas\Authentication\AuthenticationService;
use Laminas\Mvc\MvcEvent;
 
public function onBootstrap(MvcEvent $event): void
{
    $events = $event->getApplication()->getEventManager();
 
    $events->attach(MvcEvent::EVENT_ROUTE, function (MvcEvent $e) {
        $match = $e->getRouteMatch();
 
        if ($match === null) {
            return;
        }
 
        $protected = ['blog/add', 'blog/edit', 'blog/delete'];
        $auth = $e->getApplication()->getServiceManager()->get(AuthenticationService::class);
 
        if (!in_array($match->getMatchedRouteName(), $protected, true) || $auth->hasIdentity()) {
            return;
        }
 
        $response = $e->getResponse();
        $response->getHeaders()->addHeaderLine('Location', $e->getRouter()->assemble([], ['name' => 'login']));
        $response->setStatusCode(302);
 
        return $response;
    }, -100);
}

The listener takes the name of the matched route. If it is in the protected list and the visitor is not logged in, the listener returns a redirect response and the controller is not called. The number -100 is the priority: a negative value puts the listener after the built-in route matching.

Log out through /logout and open /blog/add — the browser lands on /login.

Roles and permissions

When there are several kinds of users, "logged in or not" is not enough. The laminas-permissions-acl package describes an access control list (ACL): roles, resources and permissions.

composer require laminas/laminas-permissions-acl
use Laminas\Permissions\Acl\Acl;
 
$acl = new Acl();
 
$acl->addRole('guest');
$acl->addRole('editor', 'guest');
$acl->addRole('admin', 'editor');
 
$acl->addResource('post');
 
$acl->allow('guest', 'post', 'view');
$acl->allow('editor', 'post', ['add', 'edit']);
$acl->allow('admin');
 
$acl->isAllowed('editor', 'post', 'edit');   // true
$acl->isAllowed('editor', 'post', 'delete'); // false

The second argument of addRole() is the parent: editor can do everything guest can. Calling allow('admin') without a resource allows the role everything.

TermWhat it isExample
RoleA kind of userguest, editor, admin
ResourceThe thing access is restricted topost
PrivilegeAn action on the resourceview, edit, delete

The Acl object is created in a factory and registered as a service, and the listener calls isAllowed() with the role from the user's identity instead of hasIdentity(). For large projects there is a similar package, laminas-permissions-rbac, where permissions are tied to roles without resources.

Common mistake

Hiding the "Delete" link in a view script is not protection: the address /blog/delete/5 can be typed by hand. Permissions are checked on the server — in a listener or in the action — and hiding links only makes the page more convenient.

Thanks for reading!

Donate to the site — we work hard for you.