Creating and consuming eZ Platform policies

By Mario Blažek -

Let’s go through the process of creating simple custom policies and use them in our code to check for permissions.

Roles and policies were quite a big feature of the CMS from the early days of eZ Publish CMS. After the transition to hybrid stack, half Symfony based and half legacy code, managing roles and permissions was still done inside the old admin interface. While the eZ Publish 5 had permissions checking built into Repository layer, with the release of eZ Publish kernel version 6 it was extracted as a standalone implementation of PermissionResolver service. In eZ Platform admin v1, it was quite challenging to extend interface and add new features as it required some good understanding of JavaScript and YUI library. With the arrival of the new admin interface, called admin v2, everything came back into its place. Let’s go through the process of creating simple custom policies and use them in our code to check for permissions.

Creating a simple policy

There are two ways of adding custom policies. The first one is completely PHP based and the second one that requires PHP class which points to a YAML file where policies are defined. Let’s create simple read, delete and anonymize policies for InformationCollectionBundle.

Implementing PolicyProviderInterface

Create the InformationCollectionPolicyProvider class that needs to implement PolicyProviderInterface interface. Create an implementation of the addPolicies() method by adding the required configuration to the ConfigBuilder instance passed as an argument to our method.

$configBuilder->addConfig([
            'infocollector' => [
                'read' => [],
                'delete' => [],
                'anonymize' => [],
            ],
        ]);

This will create read, delete and anonymize policies for the infocollector module. 

Complete implementation

<?php
declare(strict_types=1);

namespace Netgen\InformationCollection\PolicyProvider;

use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Configuration\ConfigBuilderInterface;
use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Security\PolicyProvider\PolicyProviderInterface;

class InformationCollectionPolicyProvider implements PolicyProviderInterface
{
    /**
     * {@inheritDoc}
     */
    public function addPolicies(ConfigBuilderInterface $configBuilder)
    {
        $configBuilder->addConfig([
            'infocollector' => [
                'read' => [],
                'delete' => [],
                'anonymize' => [],
            ],
        ]);
    }
} 

Storing policies in a YAML file

In a case you like to store your configuration in YAML files, eZ Platform developers have enabled the way of pointing PermissionResolver to check for policies in YAML configuration. Let’s define the same policies as in the previous example, but this time we will store policies in YAML file.

First, create a policies.yml file located in your bundle configuration directory. Usually, it is Resouces\config. The next step is to define the required policies inside the YAML file:

 infocollector:
    read: ~
    delete: ~
    anonymize: ~

Create an InformationCollectionYamlPolicyProvider class that needs to extend from YamlPolicyProvider class. Create a required implementation of the getFiles() method and point it to the location of the policies.yml file:

 protected function getFiles()
    {
        return [
            __DIR__ . '/../Resources/config/policies.yml',
        ];
    }

Now that everything is in place, this is how your InformationCollectionYamlPolicyProvider class should look like:

<?php
declare(strict_types=1);

namespace Netgen\InformationCollection\PolicyProvider;

use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Security\PolicyProvider\YamlPolicyProvider;

class InformationCollectionYamlPolicyProvider extends YamlPolicyProvider
{
    protected function getFiles()
    {
        return [
            __DIR__ . '/../Resources/config/policies.yml',
        ];
    }
} 

Plugging in our custom provider

Nothing will happen just by creating custom policy provider classes because eZ Platform is not aware of the existence of our policy provider. Custom policy providers don’t have a mechanism to be plugged in through the service container by tagging it with the proper tag for example. Every custom policy provider must be explicitly added by addPolicyProvider() method, available on the EzPublishCoreExtension class. The easiest way to do that is inside our Bundle class by overriding build() method:

<?php

namespace Netgen\Bundle\InformationCollectionBundle;

use Netgen\InformationCollection\PolicyProvider\InformationCollectionPolicyProvider;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;

class NetgenInformationCollectionBundle extends Bundle
{
    /**
     * {@inheritdoc}
     */
    public function build(ContainerBuilder $container)
    {
        parent::build($container);
        $eZExtension = $container->getExtension('ezpublish');
        $eZExtension->addPolicyProvider(new InformationCollectionPolicyProvider());
    }
} 

Now if you open Roles section inside the Admin tab in the new admin interface, our custom defined policies should pop up on the dropdown list,  but with very unpleasant names like role.policy.infocollector.anonymize. This can be improved easily by adding translations for our policies. Create a forms.en.yml file inside your bundle translations directory. Usually, it is Resource\translations directory:

role.policy.infocollector: "Information collection"
role.policy.infocollector.all_functions: "Information collection / All functions"
role.policy.infocollector.read: "Information collection / Read"
role.policy.infocollector.delete: "Information collection / Delete"
role.policy.infocollector.anonymize: "Information collection / Anonymize"

Clear all caches and voilà.

Checking for permissions

After policies are implemented and configured properly, we are ready to set policy checks into our code. Currently, there are a few supported ways of policy checking in controllers. One way is with the help of the isGranted() method from Symfony’s base controller class and the other one is in custom services via PermissionResolver service. There is no official support for policy checks in Twig templates, but there is a way with EzCoreExtraBundle, an excellent library developed by ex eZ core developer Jérôme Vieilledent, kudos. Please check README docs in order to install it.

Controllers

It is fairly easy to implement policy check in the controller, just instantiate Attribute object by passing in module and function identifiers and pass it to the isGranted() method:

<?php
declare(strict_types=1);

namespace Netgen\Bundle\InformationCollectionBundle\Controller\Admin;

use eZ\Bundle\EzPublishCoreBundle\Controller;
use eZ\Publish\Core\MVC\Symfony\Security\Authorization\Attribute;
use Symfony\Component\HttpFoundation\Response;

class ExampleController extends Controller
{
    public function showAction()
    {
        $attribute = new Attribute('infocollector', 'read');
        $this->denyAccessUnlessGranted($attribute);
        return new Response("You are allowed to read collected information.");
    }
} 

If the current user is not allowed to read collected information, AccessDeniedException will be thrown, triggering 403 HTTP status code. There is also an alternative and a bit easier syntax with EzCoreExtraBundle:

<?php
declare(strict_types=1);

namespace Netgen\Bundle\InformationCollectionBundle\Controller\Admin;

use eZ\Bundle\EzPublishCoreBundle\Controller;
use Symfony\Component\HttpFoundation\Response;

class ExampleController extends Controller
{
    public function showAction()
    {
        $this->denyAccessUnlessGranted('ez:infocollector:read');
        return new Response("You are allowed to read collected information.");
    }
}

Templates

As mentioned earlier, eZ Platform does not offer support for checking policies in Twig policies and that’s where EzCoreExtraBundle jumps in very nicely. Let’s take a look at this example:

{% if is_granted('ez:infocollector:read') %}

    <ul>
    {% for collected_information in collections %}
        <li>{{ collected_information }}</li>
    {% endfor %}
    </ul>

{% else %}
    <p>You are not allowed to read collected information.</p>
{% endif %} 

Imagine we want to display a list of collected information and want to be sure that only users with proper permission will be able to see the whole list. This is very useful when there is no need for limiting access at the controller level, but rather in smaller chunks of page.

Checking permissions with PermissionResolver

Let’s implement some imaginary service that will fetch all collected information, but it needs to be aware of the current user’s permissions. In order to do that, we need already mentioned PermissionResolver service, so set it as a dependency through the constructor injection. I split the code into two examples - the first one is the permission check for currently logged in user and the second one requires user ID. First, get PermissionResolver service from the Repository, then pass the module and function identifier to hasAccess() method of PermissionResolver service. It will automatically take care of retrieving current user instance:

Current user:

<?php
declare(strict_types=1);

namespace Netgen\Bundle\InformationCollectionBundle\Service;

use eZ\Publish\API\Repository\Repository;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;

class InformationCollectionService
{
    /**
     * @var \eZ\Publish\API\Repository\Repository
     */
    protected $repository;

    public function __construct(Repository $repository)
    {
        $this->repository = $repository;
    }

    public function getCollectedInformation()
    {
        $canRead = $this->repository
            ->getPermissionResolver()
            ->hasAccess(
                'infocollector',
                'read'
            );

        if ($canRead === false) {
            throw new AccessDeniedException();
        }

        // do something
    }
} 

In the second case, we are passing user ID to our service. Now we need to load user first, by calling loadUser() method on UserService. Then pass user instance along with module and function identifier to hasAccess() method of PermissionResolver service and that’s basically it:

For a specific user:

<?php
declare(strict_types=1);

namespace Netgen\Bundle\InformationCollectionBundle\Service;

use eZ\Publish\API\Repository\Repository;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;

class InformationCollectionService
{
    /**
     * @var \eZ\Publish\API\Repository\Repository
     */
    protected $repository;

    public function __construct(Repository $repository)
    {
        $this->repository = $repository;
    }

    public function getCollectedInformation(int $userId)
    {
        $user = $this->repository
            ->getUserService()
            ->loadUser($userId);

        $canRead = $this->repository
            ->getPermissionResolver()
            ->hasAccess(
                'infocollector',
                'read',
                $user
            );

        if ($canRead === false) {
            throw new AccessDeniedException();
        }

        // do something
    }
} 

Don’t forget to define InformationCollectionService class as service inside Symfony's dependency injection container:

services:
    infomation_collection.service:
        class: Netgen\Bundle\InformationCollectionBundle\Service\InformationCollectionService
        arguments:
            - "@ezpublish.api.repository"

Conclusion

That’s all, you are now ready to implement your own policies. There is an additional topic that comes along with policies, the so-called limitations. Limitations allow you to fine-tune your policies by passing additional information such as content or location value object, but let’s leave that for some other blog post.

Comments

This site uses cookies. Some of these cookies are essential, while others help us improve your experience by providing insights into how the site is being used. For more detailed information on the cookies we use, please check our Privacy Policy.