Skip to content

Commit 49c200c

Browse files
dabrtmnocon
andcommitted
IBX-11053: Describe restricting access to form submissions (#3171)
* IBX-11053: Describe restricting access to form submissions --------- Co-Authored-By: dabrt <dabrt@users.noreply.github.com> Co-Authored-By: Marek Nocoń <mnocon@users.noreply.github.com>
1 parent da26b3f commit 49c200c

10 files changed

Lines changed: 342 additions & 6 deletions

File tree

code_samples/back_office/limitation/config/append_to_services.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,18 @@ services:
1010
App\Security\Limitation\Mapper\CustomLimitationValueMapper:
1111
tags:
1212
- { name: 'ibexa.admin_ui.limitation.mapper.value', limitationType: 'CustomLimitation' }
13+
14+
App\Security\FormPolicyProvider:
15+
tags:
16+
- { name: ibexa.permissions.limitation_type }
17+
18+
App\Security\FormSubmissionsTabDecorator:
19+
parent: Ibexa\FormBuilder\Tab\LocationView\SubmissionsTab
20+
decorates: 'Ibexa\FormBuilder\Tab\LocationView\SubmissionsTab'
21+
arguments:
22+
$innerTab: '@.inner'
23+
24+
App\Security\FormSubmissionServiceDecorator:
25+
decorates: Ibexa\FormBuilder\FormSubmission\FormSubmissionService
26+
arguments:
27+
$innerService: '@App\Security\FormSubmissionServiceDecorator.inner'

code_samples/back_office/limitation/src/Kernel.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace App;
66

7+
use App\Security\FormPolicyProvider;
78
use App\Security\MyPolicyProvider;
89
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
910
use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -18,7 +19,9 @@ protected function build(ContainerBuilder $container): void
1819
// Retrieve "ibexa" container extension
1920
/** @var \Ibexa\Bundle\Core\DependencyInjection\IbexaCoreExtension $ibexaExtension */
2021
$ibexaExtension = $container->getExtension('ibexa');
21-
// Add the policy provider
22+
23+
// Add the policy provider, you can register multiple providers by calling the method repeatedly
24+
$ibexaExtension->addPolicyProvider(new FormPolicyProvider());
2225
$ibexaExtension->addPolicyProvider(new MyPolicyProvider());
2326
}
2427
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
# src/Resources/config/policies.yaml
21
custom_module:
32
custom_function_1: ~
43
custom_function_2: [CustomLimitation]
4+
form:
5+
read_submissions: ~
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace App\Security;
4+
5+
use Ibexa\Contracts\Core\Repository\ContentService;
6+
use Ibexa\Contracts\Core\Repository\PermissionResolver;
7+
use Ibexa\Contracts\Core\Repository\Values\Content\ContentInfo;
8+
use Ibexa\Contracts\FormBuilder\FieldType\Model\Form;
9+
use Ibexa\Contracts\FormBuilder\FieldType\Model\FormSubmission;
10+
use Ibexa\Contracts\FormBuilder\FieldType\Model\FormSubmissionList;
11+
use Ibexa\Contracts\FormBuilder\FormSubmission\FormSubmissionServiceInterface;
12+
use Ibexa\Core\Base\Exceptions\NotFoundException;
13+
use Ibexa\Core\Base\Exceptions\UnauthorizedException;
14+
use Ibexa\FormBuilder\FormSubmission\Gateway\FormSubmissionGateway;
15+
16+
class FormSubmissionServiceDecorator implements FormSubmissionServiceInterface
17+
{
18+
public FormSubmissionServiceInterface $innerService;
19+
public PermissionResolver $permissionResolver;
20+
public ContentService $contentService;
21+
public FormSubmissionGateway $gateway;
22+
public function __construct(FormSubmissionServiceInterface $innerService, PermissionResolver $permissionResolver, ContentService $contentService, FormSubmissionGateway $gateway)
23+
{
24+
$this->innerService = $innerService;
25+
$this->permissionResolver = $permissionResolver;
26+
$this->contentService = $contentService;
27+
$this->gateway = $gateway;
28+
}
29+
30+
public function create(ContentInfo $content, string $languageCode, Form $form, array $data): FormSubmission
31+
{
32+
return $this->innerService->create($content, $languageCode, $form, $data);
33+
}
34+
35+
public function loadById(int $id): FormSubmission
36+
{
37+
$submissions = $this->gateway->loadById($id); // First manual data fetch
38+
39+
if (empty($submissions)) {
40+
throw new NotFoundException('FormSubmission', $id);
41+
}
42+
43+
$content = $this->contentService->loadContent($submissions[0]['content_id']);
44+
if (!$this->permissionResolver->canUser('form', 'read_submissions', $content)) {
45+
throw new UnauthorizedException('form', 'read_submissions', ['contentId' => $content->getId()]); // Permission check
46+
}
47+
48+
return $this->innerService->loadById($id); // Second data fetch through inner service
49+
}
50+
51+
// The same permission check pattern is repeated in the methods below
52+
53+
public function delete(FormSubmission $submission): void
54+
{
55+
$submissionId = $submission->getId();
56+
$submissions = $this->gateway->loadById($submissionId);
57+
58+
if (empty($submissions)) {
59+
throw new NotFoundException('FormSubmission', $submissionId);
60+
}
61+
62+
$content = $this->contentService->loadContent($submissions[0]['content_id']);
63+
if (!$this->permissionResolver->canUser('form', 'read_submissions', $content)) {
64+
throw new UnauthorizedException('form', 'read_submissions', ['contentId' => $content->getId()]);
65+
}
66+
67+
$this->innerService->delete($submission);
68+
}
69+
70+
public function loadByContent(ContentInfo $content, ?string $languageCode = null, int $offset = 0, int $limit = 25): FormSubmissionList
71+
{
72+
if (!$this->permissionResolver->canUser('form', 'read_submissions', $content)) {
73+
throw new UnauthorizedException('form', 'read_submissions', ['contentId' => $content->getId()]);
74+
}
75+
76+
return $this->innerService->loadByContent($content, $languageCode, $offset, $limit);
77+
}
78+
79+
public function loadAllByContentForExport(ContentInfo $content, ?string $languageCode = null): array
80+
{
81+
if (!$this->permissionResolver->canUser('form', 'read_submissions', $content)) {
82+
throw new UnauthorizedException('form', 'read_submissions', ['contentId' => $content->getId()]);
83+
}
84+
85+
return $this->innerService->loadAllByContentForExport($content, $languageCode);
86+
}
87+
88+
public function loadHeaders(ContentInfo $content, ?string $languageCode = null): array
89+
{
90+
if (!$this->permissionResolver->canUser('form', 'read_submissions', $content)) {
91+
throw new UnauthorizedException('form', 'read_submissions', ['contentId' => $content->getId()]);
92+
}
93+
94+
return $this->innerService->loadHeaders($content, $languageCode);
95+
}
96+
97+
public function getCount(ContentInfo $content, ?string $languageCode = null): int
98+
{
99+
if (!$this->permissionResolver->canUser('form', 'read_submissions', $content)) {
100+
throw new UnauthorizedException('form', 'read_submissions', ['contentId' => $content->getId()]);
101+
}
102+
103+
return $this->innerService->getCount($content, $languageCode);
104+
}
105+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace App\Security;
4+
5+
use Ibexa\Contracts\AdminUi\Tab\ConditionalTabInterface;
6+
use Ibexa\Contracts\AdminUi\Tab\OrderedTabInterface;
7+
use Ibexa\Contracts\AdminUi\Tab\TabInterface;
8+
use Ibexa\Contracts\Core\Repository\ContentTypeService;
9+
use Ibexa\Contracts\Core\Repository\LanguageService;
10+
use Ibexa\Contracts\Core\Repository\PermissionResolver;
11+
use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface;
12+
use Ibexa\Contracts\FormBuilder\FormSubmission\FormSubmissionServiceInterface;
13+
use Ibexa\FormBuilder\FieldType\FormFactory;
14+
use Ibexa\FormBuilder\FieldType\Type;
15+
use Ibexa\FormBuilder\Tab\LocationView\SubmissionsTab;
16+
use Symfony\Contracts\Translation\TranslatorInterface;
17+
use Twig\Environment;
18+
19+
class FormSubmissionsTabDecorator extends SubmissionsTab implements TabInterface, OrderedTabInterface, ConditionalTabInterface
20+
{
21+
private SubmissionsTab $innerTab;
22+
private PermissionResolver $permissionResolver;
23+
public function __construct(
24+
Environment $twig,
25+
TranslatorInterface $translator,
26+
FormSubmissionServiceInterface $formSubmissionService,
27+
FormFactory $formFactory,
28+
ContentTypeService $contentTypeService,
29+
LanguageService $languageService,
30+
Type $formBuilderType,
31+
ConfigResolverInterface $configResolver,
32+
SubmissionsTab $innerTab,
33+
PermissionResolver $permissionResolver
34+
) {
35+
parent::__construct($twig, $translator, $formSubmissionService, $formFactory, $contentTypeService, $languageService, $formBuilderType, $configResolver);
36+
$this->innerTab = $innerTab;
37+
$this->permissionResolver = $permissionResolver;
38+
}
39+
40+
#[\Override]
41+
public function getIdentifier(): string
42+
{
43+
return $this->innerTab->getIdentifier();
44+
}
45+
46+
#[\Override]
47+
public function getName(): string
48+
{
49+
return $this->innerTab->getName();
50+
}
51+
52+
#[\Override]
53+
public function renderView(array $parameters): string
54+
{
55+
return $this->innerTab->renderView($parameters);
56+
}
57+
58+
#[\Override]
59+
public function evaluate(array $parameters): bool
60+
{
61+
/** @var \Ibexa\Contracts\Core\Repository\Values\Content\Content $content */
62+
$content = $parameters['content'];
63+
64+
return $this->innerTab->evaluate($parameters) &&
65+
$this->permissionResolver->canUser('form', 'read_submissions', $content);
66+
}
67+
68+
#[\Override]
69+
public function getOrder(): int
70+
{
71+
return $this->innerTab->getOrder();
72+
}
73+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace App\Security;
4+
5+
use Ibexa\Bundle\Core\DependencyInjection\Configuration\ConfigBuilderInterface;
6+
use Ibexa\Bundle\Core\DependencyInjection\Security\PolicyProvider\PolicyProviderInterface;
7+
use JMS\TranslationBundle\Model\Message;
8+
use JMS\TranslationBundle\Translation\TranslationContainerInterface;
9+
10+
class FormPolicyProvider implements PolicyProviderInterface, TranslationContainerInterface
11+
{
12+
public function addPolicies(ConfigBuilderInterface $configBuilder): void
13+
{
14+
$configBuilder->addConfig([
15+
'form' => [
16+
'read_submissions' => null,
17+
],
18+
]);
19+
}
20+
21+
public static function getTranslationMessages(): array
22+
{
23+
return [
24+
(new Message('role.policy.form', 'forms'))->setDesc('Forms'),
25+
(new Message('role.policy.form.all_functions', 'forms'))->setDesc('Forms / All functions'),
26+
(new Message('role.policy.form.read_submissions', 'forms'))->setDesc('Forms / Read submissions'),
27+
];
28+
}
29+
}

code_samples/back_office/limitation/translations/forms.en.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@ role.policy.custom_module: 'Custom module'
22
role.policy.custom_module.all_functions: 'Custom module / All functions'
33
role.policy.custom_module.custom_function_1: 'Custom module / Function #1'
44
role.policy.custom_module.custom_function_2: 'Custom module / Function #2'
5+
role.policy.form: 'Forms'
6+
role.policy.form.all_functions: 'Forms / All functions'
7+
role.policy.form.read_submissions: 'Forms / Read submissions'

docs/content_management/forms/form_api.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ edition: experience
99

1010
To manage form submissions created in the [Form Builder](form_builder_guide.md), use `FormSubmissionServiceInterface`.
1111

12+
!!! tip "Restricting access to form submissions"
13+
14+
By default, back office users with access to the form content item can access the form submissions.
15+
16+
If your form submissions require stricter access control than the form itself, you can introduce a [dedicated policy that manages access to submission data](custom_policies.md#restrict-access-to-form-submissions).
17+
1218
### Getting form submissions
1319

1420
To get existing form submissions, use `FormSubmissionServiceInterface::loadByContent()` (which takes a `ContentInfo` object as parameter), or `FormSubmissionServiceInterface::loadById()`.

docs/content_management/forms/form_builder_guide.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,12 @@ Here you can view the details of each submission or delete any of them.
122122

123123
The **Download submissions** button enables you to download all the submissions in a .CSV (comma-separated value) file.
124124

125+
!!! tip "Restricting access to form submissions"
126+
127+
By default, back office users with access to the form content item can access the form submissions.
128+
129+
If your form submissions require stricter access control than the form itself, you can introduce a [dedicated policy that manages access to submission data](custom_policies.md#restrict-access-to-form-submissions).
130+
125131
## Benefits
126132

127133
### General overview

0 commit comments

Comments
 (0)