Skip to content

Custom Policies

The content Repository uses Roles and Policies to give Users access to different functions of the system.

Any bundle can expose available Policies via a PolicyProvider which can be added to EzPublishCoreBundle's service container extension.

PolicyProvider

A PolicyProvider object provides a hash containing declared modules, functions and Limitations.

  • Each Policy provider provides a collection of permission modules.
  • Each module can provide functions (For example, in content/read, "content" is the module, and "read" is the function)
  • Each function can provide a collection of Limitations.

First level key is the module name which is limited to characters within the set A-Za-z0-9_, value is a hash of available functions, with function name as key. Function value is an array of available Limitations, identified by the alias declared in LimitationType service tag. If no Limitation is provided, value can be null or an empty array.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[
    "content" => [
        "read" => ["Class", "ParentClass", "Node", "Language"],
        "edit" => ["Class", "ParentClass", "Language"]
    ],
    "custom_module" => [
        "custom_function_1" => null,
        "custom_function_2" => ["CustomLimitation"]
    ],
]

Limitations need to be implemented as Limitation types and declared as services identified with ezpublish.limitationType tag. Name provided in the hash for each Limitation is the same value set in the alias attribute in the service tag.

For example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<?php declare(strict_types=1);

namespace App\Security;

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

class MyPolicyProvider implements PolicyProviderInterface
{
    public function addPolicies(ConfigBuilderInterface $configBuilder)
    {
        $configBuilder->addConfig([
             "custom_module" => [
                 "custom_function_1" => null,
                 "custom_function_2" => ["CustomLimitation"],
             ],
         ]);
    }
}

YamlPolicyProvider

An abstract class based on YAML is provided: eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Security\PolicyProvider\YamlPolicyProvider. It defines an abstract getFiles() method.

Extend YamlPolicyProvider and implement getFiles() to return absolute paths to your YAML files.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?php declare(strict_types=1);

namespace App\Security;

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

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

In src/Resources/config/policies.yaml:

1
2
3
4
# src/Resources/config/policies.yaml
custom_module:
    custom_function_1: ~
    custom_function_2: [CustomLimitation]

Translations

Provide translations for your custom policies in the forms domain.

For example, translations/forms.en.yaml:

1
2
3
4
role.policy.custom_module: 'Custom module'
role.policy.custom_module.all_functions: 'Custom module / All functions'
role.policy.custom_module.custom_function_1: 'Custom module / Function #1'
role.policy.custom_module.custom_function_2: 'Custom module / Function #2'

Extending existing Policies

A PolicyProvider may provide new functions to a module, and additional Limitations to an existing function. It's however strongly encouraged to add functions to your own Policy modules.

It's impossible to remove an existing module, function or limitation from a Policy.

Integrating the PolicyProvider into EzPublishCoreBundle

For a PolicyProvider to be active, you have to register it in the class src/Kernel.php:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<?php declare(strict_types=1);

namespace App;

use App\Security\MyPolicyProvider;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;

class Kernel extends BaseKernel
{
    use MicroKernelTrait;

    protected function build(ContainerBuilder $container): void
    {
        // Retrieve "ezpublish" container extension
        $eZExtension = $container->getExtension('ezpublish');
        // Add the policy provider
        $eZExtension->addPolicyProvider(new MyPolicyProvider());
    }
}

Custom Limitation type

For a custom module function, existing limitation types can be used or custom ones can be created.

The base of a custom limitation is a class to store values related to the usage of this limitation in roles, and another class to implement the limitation's logic.

The value class extends eZ\Publish\API\Repository\Values\User\Limitation and specifies the limitation for which it's used:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?php declare(strict_types=1);

namespace App\Security\Limitation;

use eZ\Publish\API\Repository\Values\User\Limitation;

class CustomLimitationValue extends Limitation
{
    public function getIdentifier(): string
    {
        return 'CustomLimitation';
    }
}

The type class implements eZ\Publish\SPI\Limitation\Type.

  • accept, validate and buildValue implement the logic for using the value class.
  • evaluate assesses a limitation value against the current user, the subject object, and other context objects to determine if the limitation is satisfied or not. evaluate is used, among others places, by PermissionResolver::canUser (to check if a user with access to a function can use it in its limitations) and PermissionResolver::lookupLimitations.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
<?php declare(strict_types=1);

namespace App\Security\Limitation;

use eZ\Publish\API\Repository\Exceptions\NotImplementedException;
use eZ\Publish\API\Repository\Values\Content\Query\CriterionInterface;
use eZ\Publish\API\Repository\Values\User\Limitation;
use eZ\Publish\API\Repository\Values\User\UserReference;
use eZ\Publish\API\Repository\Values\ValueObject;
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentType;
use eZ\Publish\Core\FieldType\ValidationError;
use eZ\Publish\SPI\Limitation\Type;

class CustomLimitationType implements Type
{
    public function acceptValue(Limitation $limitationValue)
    {
        if (!$limitationValue instanceof CustomLimitationValue) {
            throw new InvalidArgumentType(
                '$limitationValue',
                FieldGroupLimitation::class,
                $limitationValue
            );
        }
    }

    public function validate(Limitation $limitationValue)
    {
        $validationErrors = [];
        if (!array_key_exists('value', $limitationValue->limitationValues)) {
            $validationErrors[] = new ValidationError("limitationValues['value'] is missing.");
        } elseif (!is_bool($limitationValue->limitationValues['value'])) {
            $validationErrors[] = new ValidationError("limitationValues['value'] is not a boolean.");
        }

        return $validationErrors;
    }

    public function buildValue(array $limitationValues)
    {
        $value = false;
        if (array_key_exists('value', $limitationValues)) {
            $value = $limitationValues['value'];
        } elseif (count($limitationValues)) {
            $value = (bool)$limitationValues[0];
        }

        return new CustomLimitationValue(['limitationValues' => ['value' => $value]]);
    }

    /**
     * @param \eZ\Publish\API\Repository\Values\ValueObject[]|null $targets
     *
     * @return bool|null
     */
    public function evaluate(Limitation $value, UserReference $currentUser, ValueObject $object, array $targets = null)
    {
        if (!$value instanceof CustomLimitationValue) {
            throw new InvalidArgumentException('$value', 'Must be of type: CustomLimitationValue');
        }

        if ($value->limitationValues['value']) {
            return Type::ACCESS_GRANTED;
        }

        // If the limitation value is not set to `true`, then $currentUser, $object and/or $targets could be challenged to determine if the access is granted or not.

        return Type::ACCESS_DENIED;
    }

    public function getCriterion(Limitation $value, UserReference $currentUser): CriterionInterface
    {
        throw new NotImplementedException(__METHOD__);
    }

    public function valueSchema()
    {
        throw new NotImplementedException(__METHOD__);
    }
}

The type class is registered as a service tagged with ezpublish.limitationType and given an alias to identify it, as well as to link it to the value.

1
2
3
4
5
services:
    # …
    App\Security\Limitation\CustomLimitationType:
        tags:
            - { name: 'ezpublish.limitationType', alias: 'CustomLimitation' }

Custom Limitation type form

Form mapper

To provide support for editing custom policies in the Back Office, you need to implement EzSystems\EzPlatformAdminUi\Limitation\LimitationFormMapperInterface.

  • mapLimitationForm adds the limitation field as a child to a provided Symfony form.
  • getFormTemplate returns the path to the template to use for rendering the limitation form. Here it uses form_label and form_widget to do so.
  • filterLimitationValues is triggered when the form is submitted and can manipulate the limitation values, such as normalizing them.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php declare(strict_types=1);

namespace App\Security\Limitation\Mapper;

use eZ\Publish\API\Repository\Values\User\Limitation;
use EzSystems\EzPlatformAdminUi\Translation\Extractor\LimitationTranslationExtractor;
use EzSystems\RepositoryForms\Limitation\LimitationFormMapperInterface;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\FormInterface;

class CustomLimitationFormMapper implements LimitationFormMapperInterface
{
    public function mapLimitationForm(FormInterface $form, Limitation $data)
    {
        $form->add('limitationValues', CheckboxType::class, [
            'label' => LimitationTranslationExtractor::identifierToLabel($data->getIdentifier()),
            'required' => false,
            'data' => $data->limitationValues['value'],
            'property_path' => 'limitationValues[value]',
        ]);
    }

    public function getFormTemplate()
    {
        return '@ezdesign/limitation/custom_limitation_form.html.twig';
    }

    public function filterLimitationValues(Limitation $limitation)
    {
    }
}

And provide a template corresponding to getFormTemplate.

1
2
3
{# templates/themes/admin/limitation/custom_limitation_form.html.twig #}
{{ form_label(form.limitationValues) }}
{{ form_widget(form.limitationValues) }}

Next, register the service with the ez.limitation.formMapper tag and set the limitationType attribute to the Limitation type's identifier:

1
2
3
    App\Security\Limitation\Mapper\CustomLimitationFormMapper:
        tags:
            - { name: 'ez.limitation.formMapper', limitationType: 'CustomLimitation' }

Notable form mappers to extend

Some abstract Limitation type form mapper classes are provided to help implementing common complex Limitations.

  • MultipleSelectionBasedMapper is a mapper used to build forms for Limitation based on checkbox lists, where multiple items can be chosen. For example, it's used to build forms for Content Type Limitation, Language Limitation or Section Limitation.
  • UDWBasedMapper is used to build Limitation form where a Content/Location must be selected. For example, it's used by the Subtree Limitation form.

Value mapper

By default, without a value mapper, the Limitation value is rendered using the block ez_limitation_value_fallback of the template vendor/ezsystems/ezplatform-admin-ui/src/bundle/Resources/views/themes/admin/limitation/limitation_values.html.twig.

To customize the rendering, a value mapper eventually transforms the Limitation value and sends it to a custom template.

The value mapper implements EzSystems\EzPlatformAdminUi\Limitation\LimitationValueMapperInterface.

Its mapLimitationValue function returns the Limitation value transformed to meet the requirements of the template.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<?php declare(strict_types=1);

namespace App\Security\Limitation\Mapper;

use eZ\Publish\API\Repository\Values\User\Limitation;
use EzSystems\EzPlatformAdminUi\Limitation\LimitationValueMapperInterface;

class CustomLimitationValueMapper implements LimitationValueMapperInterface
{
    public function mapLimitationValue(Limitation $limitation)
    {
        return $limitation->limitationValues['value'];
    }
}

Then register the service with the ez.limitation.valueMapper tag and set the limitationType attribute to Limitation type's identifier:

1
2
3
    App\Security\Limitation\Mapper\CustomLimitationValueMapper:
        tags:
            - { name: 'ez.limitation.valueMapper', limitationType: 'CustomLimitation' }

When a value mapper exists for a Limitation, the rendering uses a Twig block named ez_limitation_<lower_case_identifier>_value where <lower_case_identifier> is the Limitation identifier in a lower case. In this example, ez_limitation_customlimitation_value is the block name, and CustomLimitation is the identifier.

This template receive a values variable which is the return of the mapLimitationValue function from the corresponding value mapper.

1
2
3
4
{# templates/themes/standard/limitation/custom_limitation_value.html.twig #}
{% block ez_limitation_customlimitation_value %}
    <span style="color: {{ values ? 'green' : 'red' }};">{{ values ? 'Yes' : 'No' }}</span>
{% endblock %}

To have your block found, you have to register its template. Add the template to the configuration under ezplatform.system.<SCOPE>.limitation_value_templates:

1
2
3
4
5
ezplatform:
    system:
        default:
            limitation_value_templates:
                - { template: '@ezdesign/limitation/custom_limitation_value.html.twig', priority: 0 }

Provide translations for your custom limitation form in the ezplatform_content_forms_policies domain. For example, translations/ezplatform_content_forms_policies.en.yaml:

1
policy.limitation.identifier.customlimitation: 'Custom limitation'

Checking user custom Limitation

To check if the current user has this custom limitation set to true from a custom controller:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?php declare(strict_types=1);

namespace App\Controller;

use eZ\Publish\API\Repository\PermissionResolver;
use EzSystems\EzPlatformAdminUiBundle\Controller\Controller;
use EzSystems\EzPlatformAdminUi\Permission\PermissionCheckerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class CustomController extends Controller
{
    // ...
    /** @var PermissionResolver */
    private $permissionResolver;

    /** @var PermissionCheckerInterface */
    private $permissionChecker;

    public function __construct(
        // ...,
        PermissionResolver   $permissionResolver,
        PermissionCheckerInterface $permissionChecker
    )
    {
        // ...
        $this->permissionResolver = $permissionResolver;
        $this->permissionChecker = $permissionChecker;
    }

    // Controller actions...
    public function customAction(Request $request): Response {
        // ...
        if ($this->getCustomLimitationValue()) {
            // Action only for user having the custom limitation checked
        }
    }

    private function getCustomLimitationValue(): bool {
        $customLimitationValues = $this->permissionChecker->getRestrictions($this->permissionResolver->hasAccess('custom_module', 'custom_function_2'), CustomLimitationValue::class);

        return $customLimitationValues['value'] ?? false;
    }

    public function performAccessCheck()
    {
        parent::performAccessCheck();
        $this->denyAccessUnlessGranted(new Attribute('custom_module', 'custom_function_2'));
    }
}