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.
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.
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:
init()
method will be used to load role hierarchy from database and save it in cache;isGranted()
method will be used to query the Rbac
container if the given user has the given permission
(and checking the assertion manager(s) for the dynamic assertions).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);
}
}