Как вы и сами прекрасно понимаете, пользователь должен видеть только свои заказы, свои сообщения и так далее, и ни в коем случае не чужие. Но, конечно, иногда пропустив условие выборки данных, например, забыв указать WHERE
в ParamConverter, мы нарушаем это правило.
Я покажу вам как можно легко избавиться от подобного рода проблем не только на определенных запросах и таблицах, а в пределах всего Symfony приложения. И в этом нам помогут аннотации и фильтры Доктрины.
В итоге вы повысите безопасность приложения, упростите код (не придётся прописывать отдельные запросы) и сможете быстрее расширять функционал.
Предположим, у нас есть связанные сущности User
и Order
. Пользователь должен иметь доступ только к своим заказам.
<?php
/** @Entity **/
class User
{
// ...
}
/** @Entity **/
class Order
{
// ...
/**
* @ManyToOne(targetEntity="User")
* @JoinColumn(name="user_id", referencedColumnName="id")
**/
private $user;
}
В целом, идея заключается в том, что каждый запрос на выборку заказов должен содержать в своем теле подобное условие WHERE user_id = :user_id
.
Шаг 1. Создание собственной аннотации для работы с защищенными сущностями
Здесь мы создадим свою собственную новую аннотацию для сущностей Доктрины.
Смысл её должен заключаться в следующем:
- Она должна служить флагом для Доктрины, чтобы та добавляла дополнительное условию к выборке.
- Она должна предоставлять имя поля, по которому связываются сущности. Создадим класс аннотации, который позволит передать имя поля связки пользователи и заказа:
<?php
namespace Acme\DemoBundle\Annotation;
use Doctrine\Common\Annotations\Annotation;
/**
* @Annotation
* @Target("CLASS")
*/
final class UserAware
{
public $userFieldName;
}
Шаг 2. Применение аннотации в сущности заказа
Поставим отметку о том, что сущность заказа фильтруется по полю пользователя.
<?php
namespace Acme\DemoBundle\Entity;
use Acme\DemoBundle\Annotation\UserAware;
/**
* Order entity
*
* @UserAware(userFieldName="user_id")
*/
class Order { ... }
Тут-то все и происходит. Просто сделав такую отметку, все запросы, связанные с этой сущностью (даже при присоединении сущности в подзапросах) будут дополнятся условием WHERE
.
Шаг 3. Создание класса фильтра Доктрины
Создадим и настроем новый фильтр для Доктрины. Более подробную документацию на тему вы найдете в документации к доктрине.
<?php
namespace Acme\DemoBundle\Filter;
use Doctrine\ORM\Mapping\ClassMetaData;
use Doctrine\ORM\Query\Filter\SQLFilter;
use Doctrine\Common\Annotations\Reader;
class UserFilter extends SQLFilter
{
protected $reader;
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)
{
if (empty($this->reader)) {
return '';
}
// The Doctrine filter is called for any query on any entity
// Check if the current entity is "user aware" (marked with an annotation)
$userAware = $this->reader->getClassAnnotation(
$targetEntity->getReflectionClass(),
'Acme\\DemoBundle\\Annotation\\UserAware'
);
if (!$userAware) {
return '';
}
$fieldName = $userAware->userFieldName;
try {
// Don't worry, getParameter automatically quotes parameters
$userId = $this->getParameter('id');
} catch (\InvalidArgumentException $e) {
// No user id has been defined
return '';
}
if (empty($fieldName) || empty($userId)) {
return '';
}
$query = sprintf('%s.%s = %s', $targetTableAlias, $fieldName, $userId);
return $query;
}
public function setAnnotationReader(Reader $reader)
{
$this->reader = $reader;
}
}
Шаг 4. Настроим фильтр Доктрины
Для начала включим фильтр в app/config/cofig.yml
:
doctrine:
orm:
filters:
user_filter:
class: Acme\DemoBundle\Filter\UserFilter
enabled: true
Добавим подписчика для каждого запроса, который будет инициализировать наш фильтр с данными о текущем пользователе (AcmeDemoBundle/Resources/config/services.yml
).
Реализация конфигуратора:
<?php
namespace Acme\DemoBundle\Filter;
use Symfony\Component\Security\Core\User\UserInterface;
class Configurator
{
protected $em;
protected $securityContext;
protected $reader;
public function __construct($em, $securityContext, $reader)
{
$this->em = $em;
$this->securityContext = $securityContext;
$this->reader = $reader;
}
public function onKernelRequest()
{
if ($user = $this->getUser()) {
$filter = $this->em->getFilters()->enable('user_filter');
$filter->setParameter('id', $user->getId());
$filter->setAnnotationReader($this->reader);
}
}
private function getUser()
{
$token = $this->securityContext->getToken();
if (!$token) {
return null;
}
$user = $token->getUser();
if (!($user instanceof UserInterface)) {
return null;
}
return $user;
}
}
Шаг 5. Готово
Теперь каждый запрос к таблице заказов будет содержать в своем теле условие user_id = :user_id
, будь то простой SELECT
или INNER JOIN
.
Нашу новую аннотацию можно применять к любой сущности, просто добавьте @UserAware
и ко всем запросам будет добавляться условие выборки.
А вот так теперь будет выглядеть ваш контроллер:
<?php
namespace Acme\DemoBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
class OrderController
{
/**
* Show order action
*
* @param Order $order
* @Template
*/
public function showAction(Order $order)
{
return array('order' => $order);
}
}
Все последующие запросы будут содержать условие WHERE
(допустим id связанного пользователя равно 12345):
<?php
// SELECT * FROM order WHERE id = 1 AND user_id = 12345
$this->em->getRepository('DemoAcmeBundle:Order')->find(1);
// SELECT *
// FROM order_product op
// INNER JOIN order ON (op.order_id = o.id AND o.user_id = 12345)
// WHERE o.id = 987
$this->em->getRepository('DemoAcmeBundle:OrderProduct')
->createQueryBuilder('op')
->innerJoin('op.order', 'o')
->where('o.id = :order_id')
->setParameter('order_id', 987)
;