简体   繁体   中英

How to control access to actions without override checkAccess and hasAccess methods

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.

You can use a voter

in which you check if the task is closed

switch ($attribute) {
    case self::EDIT:
        return !$subject->isClosed();
}

if you have multiple voters (yours and/or Sonata ones) take a look at the access decision strategy

for example you can set the strategy to unanimous which will make the user have access only if sonata AND your voter grant access

another example, you can define the whole logic in your voter (checking also user roles) and setting the strategy to priority and then you can give your voter a high priority so your voter will always be executed instead of Sonata ones

finally read all the options and do the most suitable solution for you because we don't know all your code context

The solution is to create and use custom SecurityHandler service for specific Admin class.

To solve my case, follow these steps:

  1. 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
    {
    }
}
  1. 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
  1. 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 }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM