# Custom policies

The content repository uses [roles and policies](https://doc.ibexa.co/en/latest/permissions/permissions/index.md) to give users access to different functions of the system.

Any bundle can expose available policies via a `PolicyProvider` which can be added to IbexaCoreBundle's [service container](https://doc.ibexa.co/en/latest/api/php_api/php_api/#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.

```
[
    "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 `ibexa.permissions.limitation_type` tag. Name provided in the hash for each limitation is the same value set in the `alias` attribute in the service tag.

For example:

```
<?php declare(strict_types=1);

namespace App\Security;

use Ibexa\Bundle\Core\DependencyInjection\Configuration\ConfigBuilderInterface;
use Ibexa\Bundle\Core\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"],
             ],
         ]);
    }
}
```

Extend existing policies

While a `PolicyProvider` may provide new functions to an existing policy module, or additional limitations to an existing function, it's however strongly recommended to create your own modules.

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

### YamlPolicyProvider

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

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

```
<?php

declare(strict_types=1);

namespace App\Security;

use Ibexa\Bundle\Core\DependencyInjection\Security\PolicyProvider\YamlPolicyProvider;

class MyPolicyProvider extends YamlPolicyProvider
{
    /** @returns string[] */
    protected function getFiles(): array
    {
        return [
            __DIR__ . '/../Resources/config/policies.yaml',
        ];
    }
}
```

In `src/Resources/config/policies.yaml`:

```
# 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`:

```
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'
```

You can also implement `TranslationContainerInterface` to provide those translations in your policy provider class:

```
<?php declare(strict_types=1);

namespace App\Security;

use Ibexa\Bundle\Core\DependencyInjection\Configuration\ConfigBuilderInterface;
use Ibexa\Bundle\Core\DependencyInjection\Security\PolicyProvider\PolicyProviderInterface;

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

    public static function getTranslationMessages(): array
    {
        return [
            (new Message('role.policy.custom_module', 'forms'))->setDesc('Custom module'),
            (new Message('role.policy.custom_module.all_functions', 'forms'))->setDesc('Custom module / All functions'),
            (new Message('role.policy.custom_module.custom_function_1', 'forms'))->setDesc('Custom module / Function #1'),
            (new Message('role.policy.custom_module.custom_function_2', 'forms'))->setDesc('Custom module / Function #2'),
        ];
    }
}
```

Then, extract this translation to generate the English translation file `translations/forms.en.xlf`:

```
php bin/console jms:translation:extract en --domain=forms --dir=src --output-dir=translations
```

## `PolicyProvider` integration into `IbexaCoreBundle`

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

```
<?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 "ibexa" container extension
        /** @var \Ibexa\Bundle\Core\DependencyInjection\IbexaCoreExtension $ibexaExtension */
        $ibexaExtension = $container->getExtension('ibexa');
        // Add the policy provider
        $ibexaExtension->addPolicyProvider(new MyPolicyProvider());
    }
}
```

## Custom limitation type

For a custom module function, you can use existing limitation types or create custom ones.

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

The value class extends `Ibexa\Contracts\Core\Repository\Values\User\Limitation` and says for which limitation it's used:

```
<?php

declare(strict_types=1);

namespace App\Security\Limitation;

use Ibexa\Contracts\Core\Repository\Values\User\Limitation;

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

The type class implements `Ibexa\Contracts\Core\Limitation\Type`.

- `accept`, `validate` and `buildValue` implement the value class usage logic.
- `evaluate` challenges a limitation value against the current user, the subject object and other context objects to return if the limitation is satisfied or not. `evaluate` is, among others, used by `PermissionResolver::canUser` (to check if a user that has access to a function can use it in its limitations) and `PermissionResolver::lookupLimitations`.

```
<?php

declare(strict_types=1);

namespace App\Security\Limitation;

use Ibexa\Contracts\Core\Limitation\Type;
use Ibexa\Contracts\Core\Repository\Exceptions\NotImplementedException;
use Ibexa\Contracts\Core\Repository\Values\Content\Query\CriterionInterface;
use Ibexa\Contracts\Core\Repository\Values\User\Limitation;
use Ibexa\Contracts\Core\Repository\Values\User\UserReference;
use Ibexa\Core\Base\Exceptions\InvalidArgumentException;
use Ibexa\Core\Base\Exceptions\InvalidArgumentType;
use Ibexa\Core\FieldType\ValidationError;

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

    /** @return \Ibexa\Contracts\Core\FieldType\ValidationError[] */
    public function validate(Limitation $limitationValue): array
    {
        $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): CustomLimitationValue
    {
        $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 \Ibexa\Contracts\Core\Repository\Values\ValueObject[]|null $targets
     *
     * @return bool|null
     */
    public function evaluate(Limitation $value, UserReference $currentUser, object $object, ?array $targets = null): ?bool
    {
        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; Here or elsewhere. When passing the baton, a limitation can return Type::ACCESS_ABSTAIN
        return Type::ACCESS_DENIED;
    }

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

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

The type class is set as a service tagged `ibexa.permissions.limitation_type` with an alias to identify it, and to link it to the value.

```
services:
    # …
    App\Security\Limitation\CustomLimitationType:
        tags:
            - { name: 'ibexa.permissions.limitation_type', alias: 'CustomLimitation' }
```

### Custom limitation type form

#### Form mapper

To provide support for editing custom policies in the back office, you need to implement [`Ibexa\AdminUi\Limitation\LimitationFormMapperInterface`](https://github.com/ibexa/admin-ui/blob/5.0/src/lib/Limitation/LimitationFormMapperInterface.php).

- `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 use [`form_label`](https://symfony.com/doc/7.4/form/form_customization.html#reference-forms-twig-label) and [`form_widget`](https://symfony.com/doc/7.4/form/form_customization.html#reference-forms-twig-widget) to do so.
- `filterLimitationValues` is triggered when the form is submitted and can manipulate the limitation values, such as normalizing them.

```
<?php

declare(strict_types=1);

namespace App\Security\Limitation\Mapper;

use Ibexa\AdminUi\Limitation\LimitationFormMapperInterface;
use Ibexa\Contracts\Core\Repository\Values\User\Limitation;
use Ibexa\Core\Limitation\LimitationIdentifierToLabelConverter;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\FormInterface;

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

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

    public function filterLimitationValues(Limitation $limitation): void
    {
    }
}
```

Provide a template corresponding to `getFormTemplate`.

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

Next, register the service with the `ibexa.admin_ui.limitation.mapper.form` tag and set the `limitationType` attribute to the limitation type's identifier:

```
    App\Security\Limitation\Mapper\CustomLimitationFormMapper:
        tags:
            - { name: 'ibexa.admin_ui.limitation.mapper.form', 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 limitations based on a checkbox list, where multiple items can be chosen. For example, it's used to build forms for [Content Type Limitation](https://doc.ibexa.co/en/latest/permissions/limitation_reference/#content-type-limitation), [Language Limitation](https://doc.ibexa.co/en/latest/permissions/limitation_reference/#language-limitation) or [Section Limitation](https://doc.ibexa.co/en/latest/permissions/limitation_reference/#section-limitation).
- `UDWBasedMapper` is used to build a limitation form where a content/location must be selected. For example, it's used by the [Subtree Limitation](https://doc.ibexa.co/en/latest/permissions/limitation_reference/#subtree-limitation) form.

#### Value mapper

By default, without a value mapper, the limitation value is rendered by using the block `ibexa_limitation_value_fallback` of the template [`vendor/ibexa/admin-ui/src/bundle/Resources/views/themes/admin/limitation/limitation_values.html.twig`](https://github.com/ibexa/admin-ui/blob/v5.0.7/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 [`Ibexa\AdminUi\Limitation\LimitationValueMapperInterface`](https://github.com/ibexa/admin-ui/blob/4.5/src/lib/Limitation/LimitationValueMapperInterface.php).

Its `mapLimitationValue` function returns the limitation value transformed for the needs of the template.

```
<?php

declare(strict_types=1);

namespace App\Security\Limitation\Mapper;

use Ibexa\AdminUi\Limitation\LimitationValueMapperInterface;
use Ibexa\Contracts\Core\Repository\Values\User\Limitation;

class CustomLimitationValueMapper implements LimitationValueMapperInterface
{
    /**
     * @return array<bool>
     */
    public function mapLimitationValue(Limitation $limitation): array
    {
        return [$limitation->limitationValues['value']];
    }
}
```

Then register the service with the `ibexa.admin_ui.limitation.mapper.value` tag and set the `limitationType` attribute to limitation type's identifier:

```
    App\Security\Limitation\Mapper\CustomLimitationValueMapper:
        tags:
            - { name: 'ibexa.admin_ui.limitation.mapper.value', limitationType: 'CustomLimitation' }
```

When a value mapper exists for a limitation, the rendering uses a Twig block named `ibexa_limitation_<lower_case_identifier>_value` where `<lower_case_identifier>` is the limitation identifier in lower case. In this example, block name is `ibexa_limitation_customlimitation_value` as the identifier is `CustomLimitation`.

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

```
{# templates/themes/standard/limitation/custom_limitation_value.html.twig #}
{% block ibexa_limitation_customlimitation_value %}
    {% set is_set = values | first %}
    <span style="color: {{ is_set ? 'green' : 'red' }};">{{ is_set ? 'Yes' : 'No' }}</span>
{% endblock %}
```

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

```
ibexa:
    system:
        default:
            limitation_value_templates:
                - { template: '@ibexadesign/limitation/custom_limitation_value.html.twig', priority: 0 }
```

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

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

### Custom limitation check

Check if current user has this custom limitation set to true from a custom controller:

```
<?php declare(strict_types=1);

namespace App\Controller;

use App\Security\Limitation\CustomLimitationValue;
use Ibexa\Contracts\AdminUi\Controller\Controller;
use Ibexa\Contracts\AdminUi\Permission\PermissionCheckerInterface;
use Ibexa\Contracts\Core\Repository\PermissionResolver;
use Ibexa\Contracts\User\Controller\AuthenticatedRememberedCheckTrait;
use Ibexa\Contracts\User\Controller\RestrictedControllerInterface;
use Ibexa\Core\MVC\Symfony\Security\Authorization\Attribute;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class CustomController extends Controller implements RestrictedControllerInterface
{
    use AuthenticatedRememberedCheckTrait {
        AuthenticatedRememberedCheckTrait::performAccessCheck as public traitPerformAccessCheck;
    }

    public function __construct(
        // ...,
        private readonly PermissionResolver $permissionResolver,
        private readonly PermissionCheckerInterface $permissionChecker
    ) {
    }

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

        return new Response('<html><body>...</body></html>');
    }

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

        if (is_bool($hasAccess)) {
            return $hasAccess;
        }

        $customLimitationValues = $this->permissionChecker->getRestrictions(
            $hasAccess,
            CustomLimitationValue::class
        );

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

    #[\Override]
    public function performAccessCheck(): void
    {
        $this->traitPerformAccessCheck();
        $this->denyAccessUnlessGranted(new Attribute('custom_module', 'custom_function_2'));
    }
}
```
