From version 3.102.0 of SonataAdminBundle a lot of methods in AbstractAdmin are marked as final. The most important (in my opinion) "checkAccess" and "hasAccess" methods are also marked as "final" and cannot be overwritten in Admin classes any more to handle access to actions on my own. How to handle cases when I want restrict access to some actions based on state of object?
For example I have "Task" entity:
<?php
class Task
{
private ?int $id = null;
private ?string $name = null;
private bool $closed = false;
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function isClosed(): bool
{
return $this->closed;
}
public function setClosed(bool $closed): self
{
$this->closed = $closed;
return $this;
}
}
I want to denied access to edit action if Task object is closed.
Before version 3.102, doing this was simple:
<?php
class TaskAdmin extends AbstractAdmin
{
protected function checkAccess($action, $object = null)
{
if ('edit' === $action && $object && $object->isClosed()) {
throw new AccessDenied('Access Denied to action edit because task is closed.');
}
parent::checkAccess($action, $object);
}
protected function hasAccess($action, $object = null)
{
if ('edit' === $action && $object && $object->isClosed()) {
return false;
}
return parent::hasAccess($action, $object);
}
}
Of course now I can't override these methods.
I thought about Voters but in this case is not perfect, because Sonata checks first if user have "Super admin role/roles". If not, then next is checked specific role (for example ROLE_ADMIN_TASK_TASK_EDIT in my case). So, user with super admin role will still be able to edit Task object even though it is closed.
Another idea was create Controller for this TaskAdmin and override "preEdit" method and check there if object is closed or not and denied access. This solution is also not perfect, because in many places in templates is fired "hasAccess" method to checks if some parts of UI should be visible or not (for example edit button), so the user will still see the edit button but will not be able to enter the edit action (prevents on controller level).
It would be perfect if there were methods for example "preCheckAccess" and "preHasAccess" that could be overwritten in Admin classes (if "checkAccess" and "hasAccess" methods must remain marked as final).
Any other ideas? Thanks for yours help.
CodePudding user response:
The solution is to create and use custom SecurityHandler service for specific Admin class.
To solve my case, follow these steps:
- Create custom SecurityHandler class:
// src/Security/Handler/TaskSecurityHandler.php
<?php
namespace App\Security\Handler;
use App\Entity\Task;
use Sonata\AdminBundle\Security\Handler\SecurityHandlerInterface;
class TaskSecurityHandler extends SecurityHandlerInterface
{
private SecurityHandlerInterface $defaultSecurityHandler;
public function __construct(SecurityHandlerInterface $defaultSecurityHandler)
{
$this->defaultSecurityHandler = $defaultSecurityHandler;
}
public function isGranted(AdminInterface $admin, $attributes, ?object $object = null): bool
{
// Handle custom access logic
if (is_string($attributes) && 'EDIT' === $attributes && $object instanceof Task && $object->isClosed()) {
return false;
}
// Leave default access logic
return $this->defaultSecurityHandler->isGranted($admin, $attributes, $object);
}
public function getBaseRole(AdminInterface $admin): string
{
return '';
}
public function buildSecurityInformation(AdminInterface $admin): array
{
return [];
}
public function createObjectSecurity(AdminInterface $admin, object $object): void
{
}
public function deleteObjectSecurity(AdminInterface $admin, object $object): void
{
}
}
- Register custom SecurityHandler class in services.yaml and inject default SecurityHandler service:
# config/services.yaml
services:
App\Security\Handler\TaskSecurityHandler:
arguments:
- '@sonata.admin.security.handler' #default SecurityHandler service configured in global configuration of SonataAdminBundle
- Use
security_handler
tag to point to your custom SecurityHandler service for specific Admin class:
# config/services.yaml
services:
# ...
app.admin.task:
class: App\Admin\TaskAdmin
arguments: [~, App\Entity\Task, ~]
tags:
- { name: sonata.admin, manager_type: orm, label: Task, security_handler: App\Security\Handler\TaskSecurityHandler }