ApiPlatform - implement authorization based on apiplatform filters - symfony

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!

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 }

Twig is_granted() with array of roles

I tried to pass a array to is_granted() twig filter but doesn't work, although in the documentation say yes:
https://symfony.com/doc/current/reference/twig_reference.html#is-granted
Imagine the app.user has the role ROLE_MANAGER.
Example:
{{ dump(is_granted('ROLE_MANAGER')) }}
This returns: true
{{ dump(is_granted(['ROLE_ADMIN','ROLE_MANAGER'])) }}
This returns: false
The documentations says:
role: type: string, string[]
Returns true if the current user has the given role. If several roles are passed in an array, true is returned if the user has at least one of them.
But this doesn't works... Any idea why?
I had the same issue. I resolved it by creating a custom Voter. For some reason is_granted function doesn't work as the documentation says. Inspecting the code I noticed that it never finds a voter for this case, which is why I ended up creating one.
Below an example of the voter that I used:
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
class MenuVoter extends Voter
{
/**
*
* #var Security
*/
private $security;
public function __construct(Security $security)
{
$this->security = $security;
}
/**
* {#inheritdoc}
*/
protected function supports($attribute, $subject): bool
{
return $subject === null && \is_array($attribute);
}
/**
* {#inheritdoc}
*/
protected function voteOnAttribute($attributes, $post, TokenInterface $token): bool
{
$user = $token->getUser();
$r = false;
if (!($user instanceof UserInterface)) {
return false;
}
foreach ($attributes as $attribute) {
if ($this->security->isGranted($attribute)) {
$r = true;
break;
}
}
return $r;
}
}
What it basically does is to loop through every role (attribute) and check if access is granted for one of them.
I hope it helps and if someone has a better way I'd love to know.

Laravel 5.8 Custom Email and password Columns

I have a WordPress running application, which I would like to access using a separate interface that uses Laravel 5.8.(don't worry about the hashing)
As such, instead of cloning passwords back and forth, I would like to use the user_email and user_pass columns in the Laravel User model instead.
I have tried what the official docs say :
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class LoginController extends Controller
{
/**
* Handle an authentication attempt.
*
* #param \Illuminate\Http\Request $request
*
* #return Response
*/
public function authenticate(Request $request)
{
$credentials = $request->only('user_email', 'user_pass');
if (Auth::attempt($credentials)) {
// Authentication passed...
return redirect()->intended('dashboard');
}
}
}
I then edited the blade files, but no avail. Any pointers?
Laravel provides a way to change the default columns for auth (email, password) by overriding some functions.
In your User model add this function that overrides the default column for password:
App/User.php
/**
* Get the password for the user.
*
* #return string
*/
public function getAuthPassword()
{
return $this->user_pass;
}
And, in your LoginController change from email to user_email
App/Http/Controllers/Auth/LoginController.php
/**
* Get the login username to be used by the controller.
*
* #return string
*/
public function username()
{
return 'user_email';
}
Now you have overridden the default columns used by Laravel's Auth logic. But you are not finished yet.
LoginController has a function that validates the user's input and the password column is hardcoded to password so in order to change that, you also need to add these functions in LoginController:
App/Http/Controllers/Auth/LoginController.php
/**
* Validate the user login request.
*
* #param \Illuminate\Http\Request $request
* #return void
*
* #throws \Illuminate\Validation\ValidationException
*/
protected function validateLogin(Request $request)
{
$request->validate([
$this->username() => 'required|string',
'user_pass' => 'required|string',
]);
}
/**
* Get the needed authorization credentials from the request.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
protected function credentials(Request $request)
{
return $request->only($this->username(), 'user_pass');
}
Next step is to create a custom Provider, let's call it CustomUserProvider that will be used instead of the default one EloquentUserProvider and where you will override the password field.
App/Providers/CustomUserProvider.php
<?php
namespace App\Providers;
class CustomUserProvider extends EloquentUserProvider
{
/**
* Retrieve a user by the given credentials.
*
* #param array $credentials
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByCredentials(array $credentials)
{
if (empty($credentials) ||
(count($credentials) === 1 &&
array_key_exists('user_pass', $credentials))) {
return;
}
// First we will add each credential element to the query as a where clause.
// Then we can execute the query and, if we found a user, return it in a
// Eloquent User "model" that will be utilized by the Guard instances.
$query = $this->createModel()->newQuery();
foreach ($credentials as $key => $value) {
if (Str::contains($key, 'user_pass')) {
continue;
}
if (is_array($value) || $value instanceof Arrayable) {
$query->whereIn($key, $value);
} else {
$query->where($key, $value);
}
}
return $query->first();
}
/**
* Validate a user against the given credentials.
*
* #param \Illuminate\Contracts\Auth\Authenticatable $user
* #param array $credentials
* #return bool
*/
public function validateCredentials(UserContract $user, array $credentials)
{
$plain = $credentials['user_pass'];
return $this->hasher->check($plain, $user->getAuthPassword());
}
}
Now that you extended the default provider you need to tell Laravel to use this one instead of EloquentUserProvider. This is how you can do it.
App/Providers/AuthServiceProvider.php
/**
* Register any authentication / authorization services.
*
* #return void
*/
public function boot()
{
$this->registerPolicies();
$this->app->auth->provider('custom', function ($app, $config) {
return new CustomUserProvider($app['hash'], $config['model']);
});
}
Finally update the config information config/auth.php and change the driver from eloquent to custom (that's how I named it above; you can change it to whatever you want). So the config/auth.php file should have this bit:
'providers' => [
'users' => [
'driver' => 'custom',
'model' => App\User::class,
],
],
Hope it helps!
Regards
It would be up and working, If you can just use sessions here instead of using Auth::attempt just like working on core PHP.

Symfony2: Call Voter from another Voter

I am using Voters to restrict access to entities in a REST API.
Step 1
Consider this voter that restricts users access to blog posts:
class BlogPostVoter extends Voter
{
public function __construct(AccessDecisionManagerInterface $decisionManager)
{
$this->decisionManager = $decisionManager;
}
/**
* Determines if the attribute and subject are supported by this voter.
*
* #param string $attribute An attribute
* #param int $subject The subject to secure, e.g. an object the user wants to access or any other PHP type
*
* #return bool True if the attribute and subject are supported, false otherwise
*/
protected function supports($attribute, $subject)
{
if (!in_array($attribute, $this->allowedAttributes)) {
return false;
}
if (!$subject instanceof BlogPost) {
return false;
}
return true;
}
/**
* Perform a single access check operation on a given attribute, subject and token.
*
* #param string $attribute
* #param mixed $subject
* #param TokenInterface $token
* #return bool
* #throws \Exception
*/
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
return $this->canUserAccess($attribute, $subject, $token);
}
public function canUserAccess($attribute, $subject, TokenInterface $token) {
if ($this->decisionManager->decide($token, array('ROLE_SUPPORT', 'ROLE_ADMIN'))) {
return true;
}
//other logic here omitted ...
return false;
}
}
You can see there is a public function canUserAccess to determine if the user is allowed to see the BlogPost. This all works just fine.
Step 2
Now I have another voter that checks something else, but also needs to check this same exact logic for BlogPosts. My thought was to:
add a new voter
perform some other checks
but then also perform this BlogPost check
So I thought I would inject the BlogPostVoter into my other voter like this:
class SomeOtherVoter extends Voter
{
public function __construct(BlogPostVoter $blogPostVoter)
{
$this->decisionManager = $decisionManager;
}
...
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
//other logic
if ($this->blogPostVoter->canUserAccess($attribute, $subject, $token)) {
return true;
}
return false;
}
}
Problem
When I do this I get the following error, using both setter and constructor injection:
Circular reference detected for service "security.access.decision_manager", path: "security.access.decision_manager"
I don't see where the security.access.decision_manager should depend on the Voter implementations. So I'm not seeing where the circular reference is.
Is there another way I can call VoterA from VoterB?
To reference VoterOne from VoterTwo you can inject the AuthorizationCheckerInterface into VoterTwo and then call ->isGranted('ONE'). Where ONE is the supported attribute of VoterOne.
Like:
class VoterTwo extends Voter
{
private $authorizationChecker;
public function __construct(AuthorizationCheckerInterface $authorizationChecker)
{
$this->authorizationChecker = $authorizationChecker;
}
protected function supports($attribute, $subject)
{
return in_array($attribute, ['TWO']);
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
return $this->authorizationChecker->isGranted('ONE', $subject);
}
}
In this example VoterTwo does just redirect the request to VoterOne (or the voter that supports the attribute ONE). This can then be extended through additional conditions.

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