- Documentation >
- Permissions >
- Custom policies
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 IbexaCoreBundle'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.
| [
"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:
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 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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | <?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:
| 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:
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 | <?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 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:
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 | <?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, you can register multiple providers by calling the method repeatedly
$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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | <?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.
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
81
82
83 | <?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\Contracts\Core\Repository\Values\ValueObject;
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\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, ValueObject $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()
{
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' }
|
To provide support for editing custom policies in the back office, you need to implement Ibexa\AdminUi\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 use 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
32
33 | <?php
declare(strict_types=1);
namespace App\Security\Limitation\Mapper;
use Ibexa\AdminUi\Limitation\LimitationFormMapperInterface;
use Ibexa\AdminUi\Translation\Extractor\LimitationTranslationExtractor;
use Ibexa\Contracts\Core\Repository\Values\User\Limitation;
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' => LimitationTranslationExtractor::identifierToLabel($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' }
|
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, Language Limitation or 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 form.
Value mapper
By default, without a value mapper, the limitation value is rendered by using the block ez_limitation_value_fallback of the template vendor/ibexa/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 Ibexa\AdminUi\Limitation\LimitationValueMapperInterface.
Its mapLimitationValue function returns the limitation value transformed for the needs of the template.
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\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 ez_limitation_<lower_case_identifier>_value where <lower_case_identifier> is the limitation identifier in lower case.
In this example, block name is ez_limitation_customlimitation_value as the identifier is CustomLimitation.
This template receives a values variable which is the return of the mapLimitationValue function from the corresponding value mapper.
| {# templates/themes/standard/limitation/custom_limitation_value.html.twig #}
{% block ez_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 ezplatform.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:
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 | <?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\Core\MVC\Symfony\Security\Authorization\Attribute;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class CustomController extends Controller
{
// ...
/** @var \Ibexa\Contracts\Core\Repository\PermissionResolver */
private $permissionResolver;
/** @var \Ibexa\Contracts\AdminUi\Permission\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
}
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;
}
public function performAccessCheck(): void
{
parent::performAccessCheck();
$this->denyAccessUnlessGranted(new Attribute('custom_module', 'custom_function_2'));
}
}
|
By default, access to a Form content item is controlled by the content/read policy.
As a result, all users who can view a form in the back office can also access its Submissions tab.
However, form submissions may require stricter access control than the form itself, for example, to conform with GDPR regulations.
To tackle this, you must separate the permissions by introducing a dedicated policy that manages access to form submission:
- define a custom policy:
form/read_submissions
- enforce the policy on the PHP API level
- enforce the policy in the back office
With this setup, users with content/read permission can view the form, but cannot see the Submissions tab, while users with form/read_submissions can access the submissions, export and manage submitted data (depending on other permissions).
Implementation notes
- This implementation uses service decoration and extends internal classes.
- Some internal methods are not publicly reusable, which may require additional calls, for example,
gateway->loadById($id) or minor workarounds.
- When upgrading, review these customizations to ensure compatibility with internal API changes.
Define custom policy
First, create the FormPolicyProvider.php policy provider that registers the new form module and the read_submissions function by injecting the custom permission into the configuration tree:
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 | <?php declare(strict_types=1);
namespace App\Security;
use Ibexa\Bundle\Core\DependencyInjection\Configuration\ConfigBuilderInterface;
use Ibexa\Bundle\Core\DependencyInjection\Security\PolicyProvider\PolicyProviderInterface;
use JMS\TranslationBundle\Model\Message;
use JMS\TranslationBundle\Translation\TranslationContainerInterface;
class FormPolicyProvider implements PolicyProviderInterface, TranslationContainerInterface
{
public function addPolicies(ConfigBuilderInterface $configBuilder): void
{
$configBuilder->addConfig([
'form' => [
'read_submissions' => null,
],
]);
}
public static function getTranslationMessages(): array
{
return [
(new Message('role.policy.form', 'forms'))->setDesc('Forms'),
(new Message('role.policy.form.all_functions', 'forms'))->setDesc('Forms / All functions'),
(new Message('role.policy.form.read_submissions', 'forms'))->setDesc('Forms / Read submissions'),
];
}
}
|
Next, extract the translations to the translations/forms.en.xlf file.
Then, register the provider in the Kernel by overriding the build() method.
Unlike standard Symfony runtime services, policy providers must be registered explicitly in the application kernel, because they are consumed during the container compilation phase.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 | <?php
declare(strict_types=1);
namespace App;
use App\Security\FormPolicyProvider;
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
{
/** @var \Ibexa\Bundle\Core\DependencyInjection\IbexaCoreExtension $ibexaExtension */
$ibexaExtension = $container->getExtension('ibexa');
// Add the policy provider, you can register multiple providers by calling the method repeatedly
$ibexaExtension->addPolicyProvider(new FormPolicyProvider());
}
}
|
Then, add a service definition to config/services.yaml:
| services:
# …
App\Security\FormPolicyProvider:
tags:
- { name: ibexa.permissions.limitation_type }
|
Finally, add the policy definition in src/Resources/config/policies.yaml:
| form:
read_submissions: ~
|
This way, after you clean the cache, the new policy becomes available when you edit the policies assigned to a Role.
Secure access on PHP API level
To enforce the policy on the PHP API level, decorate the form submission service to enforce permission checks.
In src/Security, create the FormSubmissionServiceDecorator.php file:
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109 | <?php declare(strict_types=1);
namespace App\Security;
use Ibexa\Contracts\Core\Repository\ContentService;
use Ibexa\Contracts\Core\Repository\PermissionResolver;
use Ibexa\Contracts\Core\Repository\Values\Content\ContentInfo;
use Ibexa\Contracts\FormBuilder\FieldType\Model\Form;
use Ibexa\Contracts\FormBuilder\FieldType\Model\FormSubmission;
use Ibexa\Contracts\FormBuilder\FieldType\Model\FormSubmissionList;
use Ibexa\Contracts\FormBuilder\FormSubmission\FormSubmissionServiceInterface;
use Ibexa\Core\Base\Exceptions\NotFoundException;
use Ibexa\Core\Base\Exceptions\UnauthorizedException;
use Ibexa\FormBuilder\FormSubmission\Gateway\FormSubmissionGateway;
class FormSubmissionServiceDecorator implements FormSubmissionServiceInterface
{
public FormSubmissionServiceInterface $innerService;
public PermissionResolver $permissionResolver;
public ContentService $contentService;
public FormSubmissionGateway $gateway;
public function __construct(FormSubmissionServiceInterface $innerService, PermissionResolver $permissionResolver, ContentService $contentService, FormSubmissionGateway $gateway)
{
$this->innerService = $innerService;
$this->permissionResolver = $permissionResolver;
$this->contentService = $contentService;
$this->gateway = $gateway;
}
public function create(ContentInfo $content, string $languageCode, Form $form, array $data): FormSubmission
{
return $this->innerService->create($content, $languageCode, $form, $data);
}
public function loadById(int $id): FormSubmission
{
$submissions = $this->gateway->loadById($id); // First manual data fetch
if (empty($submissions)) {
throw new NotFoundException('FormSubmission', $id);
}
$content = $this->contentService->loadContent($submissions[0]['content_id']);
if (!$this->permissionResolver->canUser('form', 'read_submissions', $content)) {
throw new UnauthorizedException('form', 'read_submissions', ['contentId' => $content->getId()]); // Permission check
}
return $this->innerService->loadById($id); // Second data fetch through inner service
}
// The same permission check pattern is repeated in the methods below
public function delete(FormSubmission $submission): void
{
$submissionId = $submission->getId();
$submissions = $this->gateway->loadById($submissionId);
if (empty($submissions)) {
throw new NotFoundException('FormSubmission', $submissionId);
}
$content = $this->contentService->loadContent($submissions[0]['content_id']);
if (!$this->permissionResolver->canUser('form', 'read_submissions', $content)) {
throw new UnauthorizedException('form', 'read_submissions', ['contentId' => $content->getId()]);
}
$this->innerService->delete($submission);
}
public function loadByContent(ContentInfo $content, ?string $languageCode = null, int $offset = 0, int $limit = 25): FormSubmissionList
{
if (!$this->permissionResolver->canUser('form', 'read_submissions', $content)) {
throw new UnauthorizedException('form', 'read_submissions', ['contentId' => $content->getId()]);
}
return $this->innerService->loadByContent($content, $languageCode, $offset, $limit);
}
public function loadAllByContentForExport(ContentInfo $content, ?string $languageCode = null): array
{
if (!$this->permissionResolver->canUser('form', 'read_submissions', $content)) {
throw new UnauthorizedException('form', 'read_submissions', ['contentId' => $content->getId()]);
}
return $this->innerService->loadAllByContentForExport($content, $languageCode);
}
public function loadHeaders(ContentInfo $content, ?string $languageCode = null): array
{
if (!$this->permissionResolver->canUser('form', 'read_submissions', $content)) {
throw new UnauthorizedException('form', 'read_submissions', ['contentId' => $content->getId()]);
}
return $this->innerService->loadHeaders($content, $languageCode);
}
public function getCount(ContentInfo $content, ?string $languageCode = null): int
{
if (!$this->permissionResolver->canUser('form', 'read_submissions', $content)) {
throw new UnauthorizedException('form', 'read_submissions', ['contentId' => $content->getId()]);
}
return $this->innerService->getCount($content, $languageCode);
}
}
|
Duplicate method calls
To perform a permission check for $content, it is fetched by gateway->loadById($id).
After permission is checked, loadById($id) is called again to prevent having to copy private method implementations into the decorator.
Then, add a service definition to config/services.yaml:
| services:
# …
App\Security\FormSubmissionServiceDecorator:
decorates: Ibexa\FormBuilder\FormSubmission\FormSubmissionService
arguments:
$innerService: '@App\Security\FormSubmissionServiceDecorator.inner'
|
This way, users can't access the submission data unless they have the form/read_submissions policy added to their role.
Secure back office access
To enforce the policy in the back office, decorate the Submissions tab to hide it when the user lacks permission.
In src/Security, create the FormSubmissionsTabDecorator.php file:
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 | <?php declare(strict_types=1);
namespace App\Security;
use Ibexa\Contracts\AdminUi\Tab\ConditionalTabInterface;
use Ibexa\Contracts\AdminUi\Tab\OrderedTabInterface;
use Ibexa\Contracts\AdminUi\Tab\TabInterface;
use Ibexa\Contracts\Core\Repository\ContentTypeService;
use Ibexa\Contracts\Core\Repository\LanguageService;
use Ibexa\Contracts\Core\Repository\PermissionResolver;
use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface;
use Ibexa\Contracts\FormBuilder\FormSubmission\FormSubmissionServiceInterface;
use Ibexa\FormBuilder\FieldType\FormFactory;
use Ibexa\FormBuilder\FieldType\Type;
use Ibexa\FormBuilder\Tab\LocationView\SubmissionsTab;
use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Environment;
class FormSubmissionsTabDecorator extends SubmissionsTab implements TabInterface, OrderedTabInterface, ConditionalTabInterface
{
private SubmissionsTab $innerTab;
private PermissionResolver $permissionResolver;
public function __construct(
Environment $twig,
TranslatorInterface $translator,
FormSubmissionServiceInterface $formSubmissionService,
FormFactory $formFactory,
ContentTypeService $contentTypeService,
LanguageService $languageService,
Type $formBuilderType,
ConfigResolverInterface $configResolver,
SubmissionsTab $innerTab,
PermissionResolver $permissionResolver
) {
parent::__construct($twig, $translator, $formSubmissionService, $formFactory, $contentTypeService, $languageService, $formBuilderType, $configResolver);
$this->innerTab = $innerTab;
$this->permissionResolver = $permissionResolver;
}
#[\Override]
public function getIdentifier(): string
{
return $this->innerTab->getIdentifier();
}
#[\Override]
public function getName(): string
{
return $this->innerTab->getName();
}
#[\Override]
public function renderView(array $parameters): string
{
return $this->innerTab->renderView($parameters);
}
#[\Override]
public function evaluate(array $parameters): bool
{
/** @var \Ibexa\Contracts\Core\Repository\Values\Content\Content $content */
$content = $parameters['content'];
return $this->innerTab->evaluate($parameters) &&
$this->permissionResolver->canUser('form', 'read_submissions', $content);
}
#[\Override]
public function getOrder(): int
{
return $this->innerTab->getOrder();
}
}
|
Then, add a service definition to config/services.yaml:
| services:
# …
App\Security\FormSubmissionsTabDecorator:
parent: Ibexa\FormBuilder\Tab\LocationView\SubmissionsTab
decorates: 'Ibexa\FormBuilder\Tab\LocationView\SubmissionsTab'
arguments:
$innerTab: '@.inner'
|
This way, users can't view the Submissions tab unless they have the form/read_submissions policy added to their role.