False "new entity found through the relationship" - Object actually exists in database - symfony

We have a service which purpose is to log user actions into database. The underlying entity ActionLog has a manyToOne relation with our User entity. Both those entities are tied to the same DBAL connection (and ORM EntityManager).
Issue is: an exception is raised when a new ActionLog entity is persisted and flushed, saying we should cascade persist the object set as the #user property because considered new:
A new entity was found through the relationship 'Doctrine\Model\ActionLog#user' that was not configured to cascade persist operations for entity: John Doe. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example #ManyToOne(..,cascade={"persist"}).
And this is annoying because the User instance actually comes straight from the database and as such isn't new at all! We expect this User object to already be "MANAGED" by the entityManager and be referenced through the identity map (in other words, the object is not "detached").
So, why would Doctrine consider the User entity instance (authenticated user) as detached/new?
Using Symfony 4.0.6 ; doctrine/orm v2.6.1, doctrine/dbal 2.6.3, doctrine/doctrine-bundle 1.8.1
ActionLog model mapping extract
Doctrine\Model\ActionLog:
type: entity
table: action_log
repositoryClass: Doctrine\Repository\ActionLogRepository
manyToOne:
user:
targetEntity: Doctrine\Model\User
id: # …
fields: # …
Log service declaration
log_manager:
class: Service\Log\LogManager
public: true
arguments:
- "#?security.token_storage"
calls:
# setter required instead of the dependency injection
# to prevent circular dependency.
- ['setEntityManager', ["#doctrine.orm.entity_manager"]]
Log service implementation - Creates new ActionLog record
<?php
namespace Service\Log;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\User\UserInterface;
use Doctrine\Model\User;
use Doctrine\Model\ActionLog;
class LogManager
{
/**
* #var ObjectManager
*/
protected $om;
/**
* #var TokenStorage
*/
protected $tokenStorage;
/**
* #var User
*/
protected $user;
/**
* #var bool
*/
protected $disabled = false;
public function __construct(TokenStorage $tokenStorage = null)
{
$this->tokenStorage = $tokenStorage;
}
public function setEntityManager(ObjectManager $om)
{
$this->om = $om;
}
public function log(string $namespace, string $action, string $message = null, array $changeSet = null)
{
$log = new ActionLog;
$log
->setNamespace($namespace)
->setAction($action)
->setMessage($message)
->setChangeset($changeSet)
;
if ($this->isDisabled()) {
return;
}
if (!$log->getUser()) {
$user = $this->getUser();
$log->setUsername(
$user instanceof UserInterface
? $user->getUsername()
: ''
);
$user instanceof User && $log->setUser($user);
}
$this->om->persist($log);
$this->om->flush();
}
public function setUser(User $user): self
{
$this->user = $user;
return $this;
}
public function getUser(): ?UserInterface
{
if (!$this->user) {
if ($token = $this->tokenStorage->getToken()) {
$this->user = $token->getUser();
}
}
return is_string($this->user) ? null : $this->user;
}
public function disable(bool $disabled = true): self
{
$this->disabled = $disabled;
return $this;
}
public function isDisabled(): bool
{
return $this->disabled;
}
}
User entity dump. As you can see infos come from the database.
User {#417 ▼
#name: "John Doe"
#email: "john_doe#example.com"
#password: "ec40577ad8057ee34ce0bb9414673bf3"
#createdAt: DateTime #1523344938 {#427 ▶}
#enabled: true
#lastLogin: null
#id: 1
}
# Associated database row
'1', 'John Doe', 'john_doe#example.com', 'ec40577ad8057ee34ce0bb9414673bf3', '2018-04-10 07:22:18', '1', '1', null

The assert was right, the User instance being passed to ActionLog::setUser method was not a known reference from the ORM point of view.
What happens: the object comes from the authentication process which unserialize the User data from session storage on each request (what suggested yceruto) and a User instance is created.
My custom userProvider should refresh the user object via the ORM but it doesn't, hence the "new reference" upon persist. I have no idea why although my UserProvider implementation lets suppose it should:
/**
* #var ObjectManager
*/
protected $em;
public function __construct(ObjectManager $em)
{
$this->em = $em;
}
public function loadUserByUsername($username)
{
$user = $this->em->getRepository(User::class)->loadUserByUsername($username);
if ($user && $user->isAdmin()) {
return $user;
}
throw new UsernameNotFoundException(
sprintf('Username "%s" does not exist.', $username)
);
}
public function refreshUser(UserInterface $user)
{
return $this->loadUserByUsername($user->getUsername());
}
public function supportsClass($class)
{
return UserInterface::class === $class;
}
This said, I managed to (temporarily) solve the issue using the ORM proxy mechanism with the help of the Doctrine\ORM\EntityManager::getReference method, this can be done since the rebuilt object from session holds the User id (primary key).
The fix consist in replacing the following instruction in the Log_manager service:
$this->user = $token->getUser();
# ↓ BECOMES ↓
$this->user = $this->om->getReference(User::class, $token->getUser()->getId());
Any idea on this? Misuse? Github issue? Whatever the reason, comments are quite welcome.

Related

Api-platform, JWT token and endpoints sending back data owned by the identified user

I'm using PHP symfony with API-platform with JWT token (through LexikJWTAuthenticationBundle), latest version as of today.
I've read quite a lot of things and I know how to do the basic stuff:
Create an API exposing my entities,
Protect certain endpoints with JWT
Protecting certain endpoints with user_roles
What I'm trying to do now is to have the API only sends back data that belongs to a user instead of simply sending back everything contained in the database and represented by an entity. I've based my work on this but this does not take into account the JWT token and I don't know how to use the token in the UserFilter class : https://api-platform.com/docs/core/filters/#using-doctrine-orm-filters
Here is my Book entity :
<?php
// api/src/Entity/Book.php
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\Put;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\GetCollection;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use App\Entity\User;
use App\Attribute\UserAware;
/** A book. */
#[ORM\Entity]
#[ApiResource(operations: [
new Get(),
new GetCollection(),
new Post(),
new Put(),
new Patch(),
new Delete()
])]
#[UserAware(userFieldName: "id")]
class Book
{
/** The id of this book. */
#[ORM\Id, ORM\Column, ORM\GeneratedValue]
private ?int $id = null;
/** The ISBN of this book (or null if doesn't have one). */
#[ORM\Column(nullable: true)]
#[Assert\Isbn]
public ?string $isbn = null;
/** The title of this book. */
#[ORM\Column]
#[Assert\NotBlank]
public string $title = '';
/** The description of this book. */
#[ORM\Column(type: 'text')]
#[Assert\NotBlank]
public string $description = '';
/** The author of this book. */
#[ORM\Column]
#[Assert\NotBlank]
public string $author = '';
/** The publication date of this book. */
#[ORM\Column(type: 'datetime')]
#[Assert\NotNull]
public ?\DateTime $publicationDate = null;
/** #var Review[] Available reviews for this book. */
#[ORM\OneToMany(targetEntity: Review::class, mappedBy: 'book', cascade: ['persist', 'remove'])]
public iterable $reviews;
#[ORM\Column(length: 255, nullable: true)]
private ?string $publisher = null;
/** The book this user is about. */
#[ORM\ManyToOne(inversedBy: 'books')]
#[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id')]
#[Assert\NotNull]
public ?User $user = null;
public function __construct()
{
$this->reviews = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getPublisher(): ?string
{
return $this->publisher;
}
public function setPublisher(?string $publisher): self
{
$this->publisher = $publisher;
return $this;
}
}
Here is my UserFilter class :
<?php
// api/src/Filter/UserFilter.php
namespace App\Filter;
use App\Attribute\UserAware;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\Filter\SQLFilter;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use App\Entity\User;
final class UserFilter extends SQLFilter
{
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
{
// The Doctrine filter is called for any query on any entity
// Check if the current entity is "user aware" (marked with an attribute)
$userAware = $targetEntity->getReflectionClass()->getAttributes(UserAware::class)[0] ?? null;
$fieldName = $userAware?->getArguments()['userFieldName'] ?? null;
if ($fieldName === '' || is_null($fieldName)) {
return '';
}
try {
$userId = $this->getParameter('id');
// Don't worry, getParameter automatically escapes parameters
} catch (\InvalidArgumentException $e) {
// No user id has been defined
return '';
}
if (empty($fieldName) || empty($userId)) {
return '';
}
return sprintf('%s.%s = %s', $targetTableAlias, $fieldName, $userId);
}
}
Here is my UserAware class :
<?php
// api/Annotation/UserAware.php
namespace App\Attribute;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
final class UserAware
{
public $userFieldName;
}
I added this to my config/packages/api_platform.yaml file:
doctrine:
orm:
filters:
user_filter:
class: App\Filter\UserFilter
enabled: true
It obviously does not work, since I'm not making the bridge between the JWT token and the filter, but I have no idea how to do it. What am I missing?
The current results I have is that the GET /api/books sends back all the books stored in the database instead of sending only the ones belonging to the JWT authenticated user.
EDIT:
And for those who want the answer for ManyToMany related entities here it is : Api-platform, filtering collection result based on JWT identified user on a ManyToMany relational entity
Instead of Doctrine Filter, you could use Doctrine Extension as described here.
In your case it would need:
Create the doctrine extension:
<?php
// api/src/Doctrine/CurrentUserExtension.php
namespace App\Doctrine;
use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Doctrine\Orm\Extension\QueryItemExtensionInterface;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\Operation;
use App\Entity\Book;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Security\Core\Security;
final class CurrentUserExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
{
private $security;
public function __construct(Security $security)
{
$this->security = $security;
}
public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, Operation $operation = null, array $context = []): void
{
$this->addWhere($queryBuilder, $resourceClass);
}
public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, Operation $operation = null, array $context = []): void
{
$this->addWhere($queryBuilder, $resourceClass);
}
private function addWhere(QueryBuilder $queryBuilder, string $resourceClass): void
{
if (Book::class !== $resourceClass || $this->security->isGranted('ROLE_ADMIN') || null === $user = $this->security->getUser()) {
return;
}
$rootAlias = $queryBuilder->getRootAliases()[0];
$queryBuilder->andWhere(sprintf('%s.user = :current_user', $rootAlias));
$queryBuilder->setParameter('current_user', $user->getId());
}
}
The main logic is in the addWhere() method:
applies only if you are dealing with Book entity (but you could extend the idea to a list of entities here)
check if the user is granted admin (if so here it skips the extension, allowing admin to fetch all books)
skip if the user isn't authenticated (you should prevent this access with firewall or security arribute in your endpoints)
Then it adds a where condition to the SQL query to filter by userId (or any other condition you'll need)
Don't forget to eanble your filter:
# api/config/services.yaml
services:
# ...
'App\Doctrine\CurrentUserExtension':
tags:
- { name: api_platform.doctrine.orm.query_extension.collection }
- { name: api_platform.doctrine.orm.query_extension.item }

ApiPlatform - implement authorization based on apiplatform filters

I'm using ApiPlatform and Symfony5
I placed a filter on the User entity to sort them by a boolean value of the class named $expose
Use case:
For the /users?expose=true route ROLE_USER can get list of every user with filter $expose set to true
For the /users/ route ROLE_ADMIN can get list of every user no matter what
Here is my User class:
/**
* #ApiResource(
* attributes={
* "normalization_context"={"groups"={"user:read", "user:list"}},
* "order"={"somefield.value": "ASC"}
* },
* collectionOperations={
* "get"={
* "mehtod"="GET",
* "security"="is_granted('LIST', object)",
* "normalization_context"={"groups"={"user:list"}},
* }
* }
* )
* #ApiFilter(ExistsFilter::class, properties={"expose"})
* #ApiFilter(SearchFilter::class, properties={
* "somefield.name": "exact"
* })
* #ORM\Entity(repositoryClass=UserRepository::class)
*/
I implement my authorization rules through UserVoter:
protected function supports($attribute, $subject): bool
{
return parent::supports($attribute, $subject) &&
($subject instanceof User ||
$this->arrayOf($subject, User::class) ||
(is_a($subject, Paginator::class) &&
$this->arrayOf($subject->getQuery()->getResult(), User::class))
);
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
{
/** #var User $user */
$user = $token->getUser();
if (!$user instanceof User) {
return false;
}
if ($this->accessDecisionManager->decide($token, [GenericRoles::ROLE_ADMIN])) {
return true;
}
switch ($attribute) {
case Actions::LIST:
break;
}
return false;
}
To recover the list of User I recover the paginator object passed through the LIST attribute and make sure the object inside the request result are of type User.
This part have been tested and work properly.
Now my issue come from the fact that both those route are essentialy the same to my voter, so my authorization rules implemented through it apply to them both.
What I would like to do would be to tell my voter that both request are different (which I thought I could do as I recover a Paginator object but doesn't seem possible) so I can treat them separately in the same switch case.
So far I havn't found a way to implement it
Is there a way to implement this kind of rules ?
Or is there another way to implement this kind of authorization ?
Thank you!
If you can live with ordinary users and admin users using the same request /users/ but getting different results,
this docs page describes a way to make the result of GET collection operations depend on the user that is logged in. I adapted it for your question:
<?php
// api/src/Doctrine/CurrentUserExtension.php
namespace App\Doctrine;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use App\Entity\Offer;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Security\Core\Security;
final class CurrentUserExtension implements QueryCollectionExtensionInterface
{
private $security;
public function __construct(Security $security)
{
$this->security = $security;
}
public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null): void
{
if (User::class !== $resourceClass || $this->security->isGranted('ROLE_ADMIN')) {
return;
}
$rootAlias = $queryBuilder->getRootAliases()[0];
$queryBuilder->andWhere("$rootAlias.expose = true");
}
}
BTW, any users that do not have ROLE_ADMIN will get the filtered result, ROLE_USER is not required.
If you choose to stick with your use case that requires users with ROLE_USER to use /users?expose=true you can make a custom CollectionDataProvider that throws a FilterValidationException:
<?php
namespace App\DataProvider;
use Symfony\Component\Security\Core\Security;
use ApiPlatform\Core\DataProvider\ContextAwareCollectionDataProviderInterface;
use ApiPlatform\Core\DataProvider\CollectionDataProviderInterface;
use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface;
use ApiPlatform\Core\Exception\FilterValidationException;
use App\Entity\User;
class UserCollectionDataProvider implements ContextAwareCollectionDataProviderInterface, RestrictedDataProviderInterface
{
/** #var CollectionDataProviderInterface */
private $dataProvider;
private $security;
/**
* #param CollectionDataProviderInterface $dataProvider The built-in orm CollectionDataProvider of API Platform
*/
public function __construct(CollectionDataProviderInterface $dataProvider, Security $security)
{
$this->dataProvider = $dataProvider;
$this->security = $security;
}
/**
* {#inheritdoc}
*/
public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
{
return User::class === $resourceClass;
}
/** throws FilterValidationException */
private function validateFilters($context)
{
if ($this->security->isGranted('ROLE_ADMIN')) {
// Allow any filters, including no filters
return;
}
if (!$this->security->isGranted('ROLE_USER')) {
throw new \LogicException('No use case has been defined for this situation');
}
$errorList = [];
if (!isset($context["filters"]["expose"]) ||
$context["filters"]["expose"] !== "true" && $context["filters"]["expose"] !== '1'
) {
$errorList[] = 'expose=true filter is required.'
throw new FilterValidationException($errorList);
}
}
/**
* {#inheritdoc}
* #throws FilterValidationException;
*/
public function getCollection(string $resourceClass, string $operationName = null, array $context = []): array
{
$this->validateFilters($context);
return $this->dataProvider->getCollection($resourceClass, $operationName, $context);
}
You do need to add the following to api/config/services.yaml:
'App\DataProvider\UserCollectionDataProvider':
arguments:
$dataProvider: '#api_platform.doctrine.orm.default.collection_data_provider'
BTW, to filter by a boolean one usually uses a BooleanFilter:
* #ApiFilter(BooleanFilter::class, properties={"expose"})
This is relevant because users with ROLE_ADMIN may try to filter by expose=false. BTW, If $expose is nullable you need to test what happens with Users that have $expose set to null
WARNING: Be aware that your security will fail silently, allowing all users access to all User entities, if the property $expose is no longer mapped or if the name of the property $expose is changed but in the UserCollectionDataProvider it is not or the Filter spec it is not!

Get current logged in user in entity

I want to create some virtual properties for an entity in an n:m relation.
I have an User, an Achievment and an AchievementUser entity. The value an user have in an Achievement is stored in the field value in the entity AchievementUser.
User -------- 1:n -------- AchievementUser -------- n:1 -------- Achievement
name:String value:Integer name:String
[...] [...]
Now I want to return the value an user have in an achievement with the achievement itself. So I need a virtual property and a method getValue() in the Achievement entity, but to get the corresponding AchievementUser object, I need the ID of the current logged in user.
How can I get this? Or is there an other possiblility to get the user value for an achievement? Thanks for your help!
Edit: I only have an API based application. Only the serializer executes the Getter method. Here is the content of my serializer file:
virtual_properties:
getValue:
serialized_name: value
type: integer
groups: ['achievement_default']
You can implement a method in the Achievement entity and pass the current authenticated user into it from your controller of twig template.
use Doctrine\Common\Collections\Criteria;
// ...
/**
* #return Collection
*/
public function getAchievementUsers(User $user)
{
$criteria = Criteria::create()->where(Criteria::expr()->eq('user', $user));
return $this->achievementUsers->matching($criteria);
}
In case of using a JMS serializer, you can add a virtual field and fill it with data using getAchievementUsers method by defining a serialization listener and injecting the TokenStorage to retrieve the current authenticated user.
<?php
namespace AppBundle\Listener\Serializer;
...
use JMS\Serializer\GenericSerializationVisitor;
use JMS\Serializer\EventDispatcher\ObjectEvent;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
class AchievementSerializerListener
{
/**
* #var User
*/
protected $currentUser;
/**
* #param TokenStorage $tokenStorage
*/
public function __construct(TokenStorage $tokenStorage)
{
$this->currentUser = $tokenStorage->getToken()->getUser();
}
/**
* #param ObjectEvent $event
*/
public function onAchievementSerialize(ObjectEvent $event)
{
if (!$this->currentUser) {
return;
}
/** #var Achievement $achievement */
$achievement = $event->getObject();
/** #var GenericSerializationVisitor $visitor */
$visitor = $event->getVisitor();
$visitor->setData(
'achievement_users',
$achievement->getAchievementUsers($this->currentUser)
);
}
}
services.yml
app.listener.serializer.achievement:
class: AppBundle\Listener\Serializer\AchievementSerializerListener
arguments:
- '#security.token_storage'
tags: [ { name: jms_serializer.event_listener, event: serializer.post_serialize, class: AppBundle\Entity\Achievement, method: onAchievementSerialize } ]

Symfony2 ManyToMany relation doesn't link 2 entities - Doctrine2 tries to persist inverse entity

Good evening,
I designed my application so that a User can subscribe to multiple events and an Event can have multiple subscribers of type User.
I opted to create a ManyToMany relationship between an entity Event and an entity User to achieve this.
More precisely, the Event entity owns the relation.
class Event implements EventInterface
{
/**
* #ORM\ManyToMany(targetEntity="FS\UserBundle\Entity\User", inversedBy="events")
*/
private $subscribers;
public function __construct()
{
$this->subscribers = new \Doctrine\Common\Collections\ArrayCollection();
}
/*
** FYI my app logic is to persist the event so i addEvent($this) to $subscriber
*/
public function addSubscriber(\FS\UserBundle\Entity\User $subscriber)
{
$this->subscribers[] = $subscriber;
$subscriber->addEvent($this);
return $this;
}
My User entity is the inverse side of the relation.
class User implements UserInterface
{
/**
* #ORM\ManyToMany(targetEntity="FS\EventBundle\Entity\Event", mappedBy="subscribers")
*/
private $events;
public function __construct()
{
$this->events = new \Doctrine\Common\Collections\ArrayCollection();
}
public function addEvent(\FS\EventBundle\Entity\Event $event)
{
$this->events[] = $event;
return $this;
}
...
I addSubscriber($user) to a new Event. Then i send the Event with my EventForm's data to this controller :
private function processForm(EventInterface $event, array $parameters, $method = "PUT")
{
$form = $this->formFactory->create(new EventType(), $event, array('method' => $method));
$form->submit($parameters, 'PATCH' !== $method);
$event = $form->getData();
$validator = $this->container->get('validator');
$listErrors = $validator->validate($event);
if ($form->isValid() && count($listErrors) === 0) {
$event->setAge();
$this->om->persist($event);
$this->om->flush($event);
}
When i persist the Event, the User doesn't reflect any changes and i get the following exception : A new entity was found through the relationship 'FS\UserBundle\Entity\User#events' that was not configured to cascade persist operations for entity [user].
Why would Doctrine2 try to re-persist the User in this case ?
Try with this:
class Event implements EventInterface
{
/**
* #ORM\ManyToMany(targetEntity="FS\UserBundle\Entity\User", inversedBy="events", cascade={"persist"})
*/
private $subscribers;

Symfony2: SonataAdminBundle - How can i get the object representing the current user inside an admin class?

I use the sonata-admin bundle.
I have the relationship with the user (FOSUserBundle) in the PageEntity.
I want to save the current user which create or change a page.
My guess is get the user object in postUpdate and postPersist methods of the admin class and this object transmit in setUser method.
But how to realize this?
On the google's group I saw
public function setSecurityContext($securityContext) {
$this->securityContext = $securityContext;
}
public function getSecurityContext() {
return $this->securityContext;
}
public function prePersist($article) {
$user = $this->getSecurityContext()->getToken()->getUser();
$appunto->setOperatore($user->getUsername());
}
but this doesn't work
In the admin class you can get the current logged in user like this:
$this->getConfigurationPool()->getContainer()->get('security.token_storage')->getToken()->getUser()
EDIT based on feedback
And you are doing it this? Because this should work.
/**
* {#inheritdoc}
*/
public function prePersist($object)
{
$user = $this->getConfigurationPool()->getContainer()->get('security.token_storage')->getToken()->getUser();
$object->setUser($user);
}
/**
* {#inheritdoc}
*/
public function preUpdate($object)
{
$user = $this->getConfigurationPool()->getContainer()->get('security.token_storage')->getToken()->getUser();
$object->setUser($user);
}
Starting with symfony 2.8, you should use security.token_storage instead of security.context to retrieve the user. Use constructor injection to get it in your admin:
public function __construct(
$code,
$class,
$baseControllerName,
TokenStorageInterface $tokenStorage
) {
parent::__construct($code, $class, $baseControllerName);
$this->tokenStorage = $tokenStorage;
}
admin.yml :
arguments:
- ~
- Your\Entity
- ~
- '#security.token_storage'
then use $this->tokenStorage->getToken()->getUser() to get the current user.
I was dealing with this issue on the version 5.3.10 of symfony and 4.2 of sonata. The answer from greg0ire was really helpful, also this info from symfony docs, here is my approach:
In my case I was trying to set a custom query based on a property from User.
// ...
use Symfony\Component\Security\Core\Security;
final class YourClassAdmin extends from AbstractAdmin {
// ...
private $security;
public function __construct($code, $class, $baseControllerName, Security $security)
{
parent::__construct($code, $class, $baseControllerName);
// Avoid calling getUser() in the constructor: auth may not
// be complete yet. Instead, store the entire Security object.
$this->security = $security;
}
// customize the query used to generate the list
protected function configureQuery(ProxyQueryInterface $query): ProxyQueryInterface
{
$query = parent::configureQuery($query);
$rootAlias = current($query->getRootAliases());
// ..
$user = $this->security->getUser();
// ...
return $query;
}
}

Resources