Add and enforce 'DUPLICATE' permissions for Saved Exports

Introduce a new 'DUPLICATE' permission in SavedExportVoter and update related logic in the controller and templates to enforce this rule. Ensure only authorized users can duplicate exports and adjust UI elements accordingly for better permission handling.
This commit is contained in:
Julien Fastré 2025-05-26 12:26:48 +02:00
parent e79d6d670b
commit e89f5e4713
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
3 changed files with 18 additions and 7 deletions

View File

@ -120,15 +120,15 @@ final readonly class SavedExportController
#[Route(path: '/exports/saved/duplicate-from-saved-export/{id}/new', name: 'chill_main_export_saved_duplicate')]
public function duplicate(SavedExport $previousSavedExport, Request $request): Response
{
if (!$this->security->isGranted(SavedExportVoter::GENERATE, $previousSavedExport)) {
throw new AccessDeniedHttpException('Not allowed to see this saved export');
}
$user = $this->security->getUser();
if (!$user instanceof User) {
throw new AccessDeniedHttpException('only regular user can create a saved export');
}
if (!$this->security->isGranted(SavedExportVoter::EDIT, $previousSavedExport)) {
throw new AccessDeniedHttpException('Not allowed to edit this saved export');
}
$savedExport = new SavedExport();
$savedExport
->setExportAlias($previousSavedExport->getExportAlias())
@ -209,7 +209,7 @@ final readonly class SavedExportController
#[Route(path: '/{_locale}/exports/saved/{savedExport}/edit-options/{exportGeneration}', name: 'chill_main_export_saved_options_edit')]
public function updateOptionsFromGeneration(SavedExport $savedExport, ExportGeneration $exportGeneration, Request $request): Response
{
if (!$this->security->isGranted(SavedExportVoter::EDIT, $savedExport)) {
if (!$this->security->isGranted(SavedExportVoter::DUPLICATE, $savedExport)) {
throw new AccessDeniedHttpException('You are not allowed to access this saved export');
}

View File

@ -30,6 +30,10 @@
<p class="card-text tags">
{% if app.user is same as saved.user %}<span class="badge bg-primary">{{ 'saved_export.Owner'|trans }}</span>{% endif %}
</p>
{% else %}
<p class="card-text tags">
Partagé par <span class="badge-user">{{ saved.user|chill_entity_render_box }}</span>
</p>
{% endif %}
<p class="card-text my-3">{{ saved.description|chill_markdown_to_html }}</p>
</div>
@ -63,7 +67,9 @@
{% endif %}
{# reminder: the controller already checked that the user can generate saved exports #}
<li><a href="{{ chill_path_add_return_path('chill_main_export_new', {'alias': saved.exportAlias,'from_saved': saved.id }) }}" class="dropdown-item"><i class="fa fa-pencil"></i> {{ 'saved_export.update_filters_aggregators_and_execute'|trans }}</a></li>
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_duplicate', {'id': saved.id}) }}" class="dropdown-item"><i class="fa fa-copy"></i> {{ 'saved_export.Duplicate'|trans }}</a></li>
{% if is_granted('CHILL_MAIN_EXPORT_SAVED_DUPLICATE', saved) %}
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_duplicate', {'id': saved.id}) }}" class="dropdown-item"><i class="fa fa-copy"></i> {{ 'saved_export.Duplicate'|trans }}</a></li>
{% endif %}
{% if is_granted('CHILL_MAIN_EXPORT_SAVED_DELETE', saved) %}
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_delete', {'id': saved.id }) }}" class="dropdown-item"><i class="fa fa-trash"></i> {{ 'Delete'|trans }}</a></li>
{% endif %}

View File

@ -15,6 +15,7 @@ use Chill\MainBundle\Entity\SavedExport;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Export\ExportManager;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
final class SavedExportVoter extends Voter
@ -25,6 +26,8 @@ final class SavedExportVoter extends Voter
final public const GENERATE = 'CHILL_MAIN_EXPORT_SAVED_GENERATE';
final public const DUPLICATE = 'CHILL_MAIN_EXPORT_SAVED_DUPLICATE';
final public const SHARE = 'CHILL_MAIN_EXPORT_SAVED_SHARE';
private const ALL = [
@ -32,9 +35,10 @@ final class SavedExportVoter extends Voter
self::EDIT,
self::GENERATE,
self::SHARE,
self::DUPLICATE,
];
public function __construct(private readonly ExportManager $exportManager) {}
public function __construct(private readonly ExportManager $exportManager, private readonly AccessDecisionManagerInterface $accessDecisionManager) {}
protected function supports($attribute, $subject): bool
{
@ -52,6 +56,7 @@ final class SavedExportVoter extends Voter
return match ($attribute) {
self::DELETE, self::EDIT, self::SHARE => $subject->getUser() === $token->getUser(),
self::DUPLICATE => $this->accessDecisionManager->decide($token, [ChillExportVoter::COMPOSE_EXPORT]) && $this->accessDecisionManager->decide($token, [self::EDIT], $subject) ,
self::GENERATE => $this->canUserGenerate($user, $subject),
default => throw new \UnexpectedValueException('attribute not supported: '.$attribute),
};