Symfony 4.2 / API-платформа: недетерминированные ошибки, связанные с безопасностью - PullRequest
1 голос
/ 25 марта 2019

Мы разрабатываем REST API, используя symfony 4.2 и API-платформу, и у нас странное поведение.Когда мы выдаем, например, запросы GET HTTP на сгенерированные конечные точки: мы получаем либо ожидаемый ответ JSON, либо ошибку 500 для того же запроса, случайным образом, как кажется.Интересно, что:

  • при использовании нашего углового клиента
  • случаются случайные ошибки, которые НИКОГДА не отображаются при выдаче команд curl для одних и тех же URL

Здесьэто начало возвращенной трассировки стека JSON при возникновении ошибки:

{
  "type": "https://tools.ietf.org/html/rfc2616#section-10",
  "title": "An error occurred",
  "detail": "The class 'App\\Security\\User\\AppUser' was not found in the chain configured namespaces App\\Entity, Vich\\UploaderBundle\\Entity",
  "trace": [
    {
      "namespace": "",
      "short_class": "",
      "class": "",
      "type": "",
      "function": "",
      "file": "/home/beta/www/cel2-dev/cel2-services/vendor/doctrine/persistence/lib/Doctrine/Common/Persistence/Mapping/MappingException.php",
      "line": 22,
      "args": []
    },
    {
      "namespace": "Doctrine\\Common\\Persistence\\Mapping",
      "short_class": "MappingException",
      "class": "Doctrine\\Common\\Persistence\\Mapping\\MappingException",
      "type": "::",
      "function": "classNotFoundInNamespaces",
      "file": "/home/beta/www/cel2-dev/cel2-services/vendor/doctrine/persistence/lib/Doctrine/Common/Persistence/Mapping/Driver/MappingDriverChain.php",
      "line": 87,
      "args": [
        [
          "string",
          "App\\Security\\User\\AppUser"
        ],
        [
          "array",
          [
            [
              "string",
              "App\\Entity"
            ],
            [
              "string",
              "Vich\\UploaderBundle\\Entity"
            ]
          ]
        ]
      ]
    },
    {
      "namespace": "Doctrine\\Common\\Persistence\\Mapping\\Driver",
      "short_class": "MappingDriverChain",
      "class": "Doctrine\\Common\\Persistence\\Mapping\\Driver\\MappingDriverChain",
      "type": "->",
      "function": "loadMetadataForClass",
      "file": "/home/beta/www/cel2-dev/cel2-services/vendor/doctrine/orm/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php",
      "line": 151,
      "args": [
        [
          "string",
          "App\\Security\\User\\AppUser"
        ],
        [
          "object",
          "Doctrine\\ORM\\Mapping\\ClassMetadata"
        ]
      ]
    },
    {
      "namespace": "Doctrine\\ORM\\Mapping",
      "short_class": "ClassMetadataFactory",
      "class": "Doctrine\\ORM\\Mapping\\ClassMetadataFactory",
      "type": "->",
      "function": "doLoadMetadata",
      "file": "/home/beta/www/cel2-dev/cel2-services/vendor/doctrine/persistence/lib/Doctrine/Common/Persistence/Mapping/AbstractClassMetadataFactory.php",
      "line": 305,
      "args": [
        [
          "object",
          "Doctrine\\ORM\\Mapping\\ClassMetadata"
        ],
        [
          "null",
          null
        ],
        [
          "boolean",
          false
        ],
        [
          "array",
          []
        ]
      ]
    },
    {
      "namespace": "Doctrine\\Common\\Persistence\\Mapping",
      "short_class": "AbstractClassMetadataFactory",
      "class": "Doctrine\\Common\\Persistence\\Mapping\\AbstractClassMetadataFactory",
      "type": "->",
      "function": "loadMetadata",
      "file": "/home/beta/www/cel2-dev/cel2-services/vendor/doctrine/orm/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php",
      "line": 78,
      "args": [
        [
          "string",
          "App\\Security\\User\\AppUser"
        ]
      ]
    },
    {
      "namespace": "Doctrine\\ORM\\Mapping",
      "short_class": "ClassMetadataFactory",
      "class": "Doctrine\\ORM\\Mapping\\ClassMetadataFactory",
      "type": "->",
      "function": "loadMetadata",
      "file": "/home/beta/www/cel2-dev/cel2-services/vendor/doctrine/persistence/lib/Doctrine/Common/Persistence/Mapping/AbstractClassMetadataFactory.php",
      "line": 183,
      "args": [
        [
          "string",
          "App\\Security\\User\\AppUser"
        ]
      ]
    },
    {
      "namespace": "Doctrine\\Common\\Persistence\\Mapping",
      "short_class": "AbstractClassMetadataFactory",
      "class": "Doctrine\\Common\\Persistence\\Mapping\\AbstractClassMetadataFactory",
      "type": "->",
      "function": "getMetadataFor",
      "file": "/home/beta/www/cel2-dev/cel2-services/vendor/doctrine/orm/lib/Doctrine/ORM/EntityManager.php",
      "line": 283,
      "args": [
        [
          "string",
          "App\\Security\\User\\AppUser"
        ]
      ]
    },
    {
      "namespace": "Doctrine\\ORM",
      "short_class": "EntityManager",
      "class": "Doctrine\\ORM\\EntityManager",
      "type": "->",
      "function": "getClassMetadata",
      "file": "/home/beta/www/cel2-dev/cel2-services/vendor/doctrine/doctrine-bundle/Repository/ContainerRepositoryFactory.php",
      "line": 45,
      "args": [
        [
          "string",
          "App\\Security\\User\\AppUser"
        ]
      ]
    },
    {
      "namespace": "Doctrine\\Bundle\\DoctrineBundle\\Repository",
      "short_class": "ContainerRepositoryFactory",
      "class": "Doctrine\\Bundle\\DoctrineBundle\\Repository\\ContainerRepositoryFactory",
      "type": "->",
      "function": "getRepository",
      "file": "/home/beta/www/cel2-dev/cel2-services/vendor/doctrine/orm/lib/Doctrine/ORM/EntityManager.php",
      "line": 713,
      "args": [
        [
          "object",
          "Doctrine\\ORM\\EntityManager"
        ],
        [
          "string",
          "App\\Security\\User\\AppUser"
        ]
      ]
    }

    ...
}

Для простоты мы реализовали фиктивный объект AbstractGuardAuthenticator, который всегда возвращает пользователя с жестким кодом и демонстрирует то же поведение.Ниже следует его код, код класса пользовательского объекта вместе с интересными битами конфигурации безопасности:

<?php

namespace App\Security;

use App\Security\User\AppUser;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserProviderInterface;

class YesAuthenticator extends AbstractGuardAuthenticator
{

    public function supports(Request $request)
    {
        return true;
    }

    public function getCredentials(Request $request)
    {
        return array(
            'token' => 'sdflsdklfjsdlkfjslkdfjsldk46541qsdf',
        );
    }

    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        $user = new AppUser(22, 'toto@wanadoo.fr', 'toto', 'litoto', 'tl', 'totoL', '', array(), null);
        return $user;
    }

    public function checkCredentials($credentials, UserInterface $user)
    {
        return true;
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        return null;
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    {
        $data = array(
            'message' => strtr($exception->getMessageKey(), $exception->getMessageData())
        );

        return new JsonResponse($data, Response::HTTP_FORBIDDEN);
    }

    public function start(Request $request, AuthenticationException $authException = null)
    {
        $data = array(
            'message' => 'Authentication Required'
        );

        return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
    }

    public function supportsRememberMe()
    {
        return false;
    }

}

Класс пользовательского объекта:

<?php

namespace App\Security\User;

use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\EquatableInterface;

/**
 * Represents a user logged in the SSO authentication system.  
 *
 * @package App\Security\User
 */
class AppUser implements UserInterface, EquatableInterface {

    private $id;
    private $email;
    private $pseudo;
    private $avatar;
    private $surname;
    private $lastName;
    private $usePseudo;
    private $administeredProjectId;
    private $roles;

    const ADMIN_ROLE = "administrator";

    public function __construct(
        $id, $email, $surname, $lastName, $pseudo, $usePseudo, $avatar, 
        array $roles, $administeredProjectId) {

        $this->id = $id;
        $this->email = $email;
        $this->surname = $surname;
        $this->lastName = $lastName;
        $this->pseudo = $pseudo;
        $this->usePseudo = $usePseudo;
        $this->avatar = $avatar;
        $this->administeredProjectId = $administeredProjectId;
        $this->roles = $roles;
    }

    public function setId($idd) {

        $this->id = $idd;
    }

    public function getId() {

        return $this->id;
    }

    public function isAdmin() {
        return in_array(AppUser::ADMIN_ROLE, $this->roles);
    }

    public function isProjectAdmin() {
        return (!is_null($this->administeredProjectId));
    }

    public function isLuser() {
        return ( 
            !( $this->isAdmin() ) || 
            ( $this->isProjectAdmin() ) );
    }

    public function getRoles() {
        return $this->roles;
    }

    public function getSurname() {
        return $this->surname;
    }

    public function getLastName() {
        return $this->lastName;
    }

    public function getAvatar() {
        return $this->avatar;
    }

    public function getAdministeredProjectId() {
        return $this->administeredProjectId;
    }

    public function getPassword() {

        return null;
    }

    public function getSalt() {
        return null;
    }

    public function getEmail() {
        return $this->email;
    }

    public function getUsername() {
        return $this->usePseudo ? $this->pseudo : ($this->surname . ' ' . $this->lastName);
    }

    public function getPseudo() {
        return $this->pseudo;
    }

    public function eraseCredentials() {
    }

    public function isEqualTo(UserInterface $user) {
        if (!$user instanceof AppUser) {
            return false;
        }

        if ($this->username !== $user->getUsername()) {
            return false;
        }

        return true;
    }
}

Извлечение безопасности.yaml file:

security:
    providers:
        user:
            entity:
                class: App\Security\User\AppUser
                property: username
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: true
            guard:
                authenticators:
                     - App\Security\YesAuthenticator
        main:
            anonymous: ~
            logout: ~

            guard:
                authenticators:
                    - App\Security\YesAuthenticator

Вот пример сущности / ресурса, для которого веб-сервисы демонстрируют поведение.Ничего особенного ...

<?php

namespace App\Entity;

use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use ApiPlatform\Core\Annotation\ApiResource;

/**
 * @ORM\Entity
 * @ORM\Table(name="tb_project")
 * @ApiResource(attributes={
 *     "normalization_context"={"groups"={"read"}},
 *     "formats"={"jsonld", "json"},
 *     "denormalization_context"={"groups"={"write"}}},
 *     collectionOperations={"get"},
 *     itemOperations={"get"}
 * )
 */
class Project
{

   /**
    * @Groups({"read"})
    * @ORM\Id
    * @ORM\GeneratedValue(strategy="IDENTITY")
    * @ORM\Column(type="integer")
    */
   private $id = null;

    /**
     * @Groups({"read"})
     * @ORM\OneToOne(targetEntity="Project")
     * @ORM\JoinColumn(name="parent_id", referencedColumnName="id")
     */
    private $parent;

   /**
    * @Assert\NotNull
    * @Groups({"read"})
    * @ORM\Column(type="string", nullable=false)
    */
   private $label = null;


   /**
    *
    * @Assert\NotNull
    * @Groups({"read"})
    * @ORM\Column(name="is_private", type="boolean", nullable=false)
    */
   private $isPrivate = true;    

    public function getId(): ?int {
        return $this->id;
    }

    public function getLabel(): ?string
    {
        return $this->label;
    }

    public function setLabel(string $label): self
    {
        $this->label = $label;

        return $this;
    }

    public function getIsPrivate(): ?bool
    {
        return $this->isPrivate;
    }

    public function setIsPrivate(bool $isPrivate): self
    {
        $this->isPrivate = $isPrivate;

        return $this;
    }

    public function getParent(): ?self
    {
        return $this->parent;
    }

    public function setParent(?self $parent): self
    {
        $this->parent = $parent;

        return $this;
    }

    public function __clone() {
        if ($this->id) {
            $this->id = null;
        }
    }

    public function __toString()
    {
        return  $this->getLabel();
    }

}

Обратите внимание, что у нас есть другие веб-службы с настраиваемыми поставщиками данных, которые фильтруют возвращаемые данные на основе текущего пользователя.Недетерминированным способом запросы к ним также иногда терпят неудачу, потому что текущий пользователь является нулем (в то время как пользователь действительно зарегистрирован).

...