A free and open-source book on ZF3 for beginners


17.10. Implementing RbacManager

The next thing we will discuss will be the functionality for creating the Rbac container, whose purpose is loading the role hierarchy from the database, and caching the data in the filesystem cache.

The cache allows to store frequently used data in fast storage. For example, retrieving roles and permissions from database on each page load may be rather slow, while storing the precomputed role hierarchy in a file may be faster.

17.10.1. Setting Up Caching

First, let's set up caching. To do that, you need to install the Zend\Cache and Zend\Serializer components with the following commands:

php composer.phar require zendframework/zend-cache
php composer.phar require zendframework/zend-serializer

Finally, edit the config/autoload/global.php and add the following lines:

use Zend\Cache\Storage\Adapter\Filesystem;

return [
    //...
    // Cache configuration.
    'caches' => [
        'FilesystemCache' => [
            'adapter' => [
                'name'    => Filesystem::class,
                'options' => [
                    // Store cached data in this directory.
                    'cache_dir' => './data/cache',
                    // Store cached data for 1 hour.
                    'ttl' => 60*60*1 
                ],
            ],
            'plugins' => [
                [
                    'name' => 'serializer',
                    'options' => [                        
                    ],
                ],
            ],
        ],
    ],
    //...
];

This will allow you to use the Filesystem cache and store cached data in APP_DIR/data/cache directory.

If you want to learn more about caching, please refer to the Zend\Cache ZF3 component documentation.

17.10.2. Writing the RbacManager Service

The purpose of the RbacManager service will be to construct the Rbac container and load the roles and permissions from database. If the needed information is already saved to cache, it will load it from cache instead of loading from database.

Another goal of the RbacManager service will be to use the assertion manager we wrote earlier and check for dynamic assertions.

The RbacManager class will have two methods:

The RbacManager class will read the configuration and look for the rbac_manager key. The key should contain the assertions subkey, in which you can register all assertion managers that you have.

return [
    //...
    
    // This key stores configuration for RBAC manager.
    'rbac_manager' => [
        'assertions' => [Service\RbacAssertionManager::class],
    ],
];

The code of the RbacManager class living in User\Service namespace is presented below.

<?php
namespace User\Service;

use Zend\Permissions\Rbac\Rbac;
use Zend\Permissions\Rbac\Role as RbacRole;
use User\Entity\User;
use User\Entity\Role;
use User\Entity\Permission;

/**
 * This service is responsible for initialzing RBAC (Role-Based Access Control).
 */
class RbacManager 
{
    /**
     * Doctrine entity manager.
     * @var Doctrine\ORM\EntityManager
     */
    private $entityManager; 
    
    /**
     * RBAC service.
     * @var Zend\Permissions\Rbac\Rbac
     */
    private $rbac;
    
    /**
     * Auth service.
     * @var Zend\Authentication\AuthenticationService 
     */
    private $authService;
    
    /**
     * Filesystem cache.
     * @var Zend\Cache\Storage\StorageInterface
     */
    private $cache;
    
    /**
     * Assertion managers.
     * @var array
     */
    private $assertionManagers = [];
    
    /**
     * Constructs the service.
     */
    public function __construct($entityManager, $authService, $cache, $assertionManagers) 
    {
        $this->entityManager = $entityManager;
        $this->authService = $authService;
        $this->cache = $cache;
        $this->assertionManagers = $assertionManagers;
    }
    
    /**
     * Initializes the RBAC container.
     */
    public function init($forceCreate = false)
    {
        if ($this->rbac!=null && !$forceCreate) {
            // Already initialized; do nothing.
            return;
        }
        
        // If user wants us to reinit RBAC container, clear cache now.
        if ($forceCreate) {
            $this->cache->removeItem('rbac_container');
        }
        
        // Try to load Rbac container from cache.
        $this->rbac = $this->cache->getItem('rbac_container', $result);
        if (!$result)
        {
            // Create Rbac container.
            $rbac = new Rbac();
            $this->rbac = $rbac;

            // Construct role hierarchy by loading roles and permissions from database.

            $rbac->setCreateMissingRoles(true);

            $roles = $this->entityManager->getRepository(Role::class)
                    ->findBy([], ['id'=>'ASC']);
            foreach ($roles as $role) {

                $roleName = $role->getName();

                $parentRoleNames = [];
                foreach ($role->getParentRoles() as $parentRole) {
                    $parentRoleNames[] = $parentRole->getName();
                }

                $rbac->addRole($roleName, $parentRoleNames);

                foreach ($role->getPermissions() as $permission) {
                    $rbac->getRole($roleName)->addPermission($permission->getName());
                }
            }
            
            // Save Rbac container to cache.
            $this->cache->setItem('rbac_container', $rbac);
        }
    }
    
    /**
     * Checks whether the given user has permission.
     * @param User|null $user
     * @param string $permission
     * @param array|null $params
     */
    public function isGranted($user, $permission, $params = null)
    {
        if ($this->rbac==null) {
            $this->init();
        }
        
        if ($user==null) {
            
            $identity = $this->authService->getIdentity();
            if ($identity==null) {
                return false;
            }
            
            $user = $this->entityManager->getRepository(User::class)
                    ->findOneByEmail($identity);
            if ($user==null) {
                // Oops.. the identity presents in session, but there is no such user in database.
                // We throw an exception, because this is a possible security problem.
                throw new \Exception('There is no user with such identity');
            }
        }
        
        $roles = $user->getRoles();
        
        foreach ($roles as $role) {
            if ($this->rbac->isGranted($role->getName(), $permission)) {
                
                if ($params==null)
                    return true;
                    
                foreach ($this->assertionManagers as $assertionManager) {
                    if ($assertionManager->assert($this->rbac, $permission, $params))
                        return true;
                }
            }
            
            $parentRoles = $role->getParentRoles();
            foreach ($parentRoles as $parentRole) {
                if ($this->rbac->isGranted($parentRole->getName(), $permission)) {
                    return true;
                }
            }
        }
        
        return false;
    }
}

The factory for the RbacManager class looks like the following:

<?php
namespace User\Service\Factory;

use Interop\Container\ContainerInterface;
use User\Service\RbacManager;
use Zend\Authentication\AuthenticationService;

/**
 * This is the factory class for RbacManager service. The purpose of the factory
 * is to instantiate the service and pass it dependencies (inject dependencies).
 */
class RbacManagerFactory
{
    /**
     * This method creates the RbacManager service and returns its instance. 
     */
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {        
        $entityManager = $container->get('doctrine.entitymanager.orm_default');
        $authService = $container->get(\Zend\Authentication\AuthenticationService::class);
        $cache = $container->get('FilesystemCache');
        
        $assertionManagers = [];
        $config = $container->get('Config');
        if (isset($config['rbac_manager']['assertions'])) {
            foreach ($config['rbac_manager']['assertions'] as $serviceName) {
                $assertionManagers[$serviceName] = $container->get($serviceName);
            }
        }
        
        return new RbacManager($entityManager, $authService, $cache, $assertionManagers);
    }
}

Top