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.
The solution is to create and use custom SecurityHandler service for specific Admin class.
To solve my case, follow these steps:
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
{
}
}
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
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 }
I am developing my first Symfony 4 application and I migrating from Symfony 2+ and symfony 3+.
Right now I am developing a back-end and all of my entity classes have a addedBy() and updatedBy() methods where I need to record the current logged in administrator.
I would like to have something like an event listener where I do not have to set those methods in all of my controllers.
How to accomplish this?
First, to simplify matters and help down the road I would create an interface this user-tracking entities would need to comply with:
interface UserTracking
{
public function addedBy(UserInterface $user);
public function updatedby(UserInterface $user);
public function getAddedBy(): ?UserInterface;
public function getUpdatedBy(): ?UserInterface;
}
Then you can create a Doctrine event listener, and inject the Security component there:
class UserDataListener
{
protected $security;
public function __construct(Security $security)
{
$this->security = $security;
}
public function prePersist(LifecycleEventArgs $event): void
{
$entity = $event->getObject();
$user = $this->security->getUser();
// only do stuff if $entity cares about user data and we have a logged in user
if ( ! $entity instanceof UserTracking || null === $user ) {
return;
}
$this->setUserData($entity, $user);
}
private function preUpdate(LifecycleEventArgs $event) {
$this->prePersist($event);
}
private function setUserData(UserTracking $entity, UserInterface $user)
{
if (null === $entity->getAddedBy()) {
$entity->addedBy($user);
}
$entity->updatedBy($user);
}
}
You'd need to tag the listener appropriately, so it triggers on prePersist and preUpdate:
services:
user_data_listener:
class: App\Infrastructure\Doctrine\Listener\UserDataListener
tags:
- { name: doctrine.event_listener, event: prePersist }
- { name: doctrine.event_listener, event: preUpdate }
While the above should work, I believe it's generally not such a great idea to use Doctrine events this way, since you are coupling your domain logic with Doctrine, and are hiding changing under a layer of magic that may not be immediately evident for other developers working with your application.
I'd put the createdBy as a constructor parameter, and set updateBy explicitly when needed. It's just one line of code each time, but you gain clarity and expressiveness, and you have a simpler system with less moving parts.
class FooEntity
{
private $addedBy;
private $updatedBy;
public function __construct(UserInterface $user)
{
$this->addedBy = $user;
}
public function updatedBy(UserInterface $user)
{
$this->updatedBy = $user;
}
}
This expresses much better what's happening with the domain, and future coders in your application do no have to dig around for the possible extensions you may have installed and enabled, or what events are being triggered.
You probably do not want to reinvent the wheel. There is the Blameable Doctrine Extension that already does that. You can install it with :
composer require antishov/doctrine-extensions-bundle
As there is already a recipe to configure it. Then you can activate the extension and then use it with something like :
Specific documentation for the Blameable extension can be found here
use Gedmo\Mapping\Annotation as Gedmo;
class Post {
/**
* #var User $createdBy
*
* #Gedmo\Blameable(on="create")
* #ORM\ManyToOne(targetEntity="App\Entity\User")
*/
private $createdBy;
}
I would like to be able to get the current logged in user's credentials (email, password, etc) from the container. So, this is what I did:
security.token:
class: Symfony\Component\Security\Core\Authentication\Token\TokenInterface
factory: ["#security.token_storage", "getToken"]
private: true
security.current_user_credentials:
class: Symfony\Component\Security\Core\User\UserInterface
factory: ["#security.token", "getUser"]
security.current_user:
class: AppBundle\Entity\User
factory: ["#security.current_user_credentials", "getUser"]
When I do this and I'm logged in, it works fine. However, when I'm logged out, I get this in dev.log:
[2015-06-22 12:28:11] php.CRITICAL: Fatal Error: Call to a member function getUser() on string {"type":1,"file":"/var/www/html/phoenix/app/cache/dev/appDevDebugProjectContainer.php","line":3107,"level":-1,"stack":[{"function":"getSecurity_CurrentUserService","type":"->","class":"appDevDebugProjectContainer","file":"/var/www/html/phoenix/app/bootstrap.php.cache","line":2140,"args":[]},{"function":"get","type":"->","class":"Symfony\\Component\\DependencyInjection\\Container","file":"/var/www/html/phoenix/app/cache/dev/appDevDebugProjectContainer.php","line":674,"args":[]},{"function":"getCommandHistoryCreatorService","type":"->","class":"appDevDebugProjectContainer","file":"/var/www/html/phoenix/app/bootstrap.php.cache","line":2140,"args":[]},{"function":"get","type":"->","class":"Symfony\\Component\\DependencyInjection\\Container","file":"/var/www/html/phoenix/app/cache/dev/classes.php","line":1929,"args":[]},{"function":"lazyLoad","type":"->","class":"Symfony\\Component\\EventDispatcher\\ContainerAwareEventDispatcher","file":"/var/www/html/phoenix/app/cache/dev/classes.php","line":1894,"args":[]},{"function":"getListeners","type":"->","class":"Symfony\\Component\\EventDispatcher\\ContainerAwareEventDispatcher","file":"/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php","line":99,"args":[]},{"function":"getListeners","type":"->","class":"Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcher","file":"/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php","line":158,"args":[]},{"function":"getNotCalledListeners","type":"->","class":"Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcher","file":"/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/DataCollector/EventDataCollector.php","line":48,"args":[]},{"function":"lateCollect","type":"->","class":"Symfony\\Component\\HttpKernel\\DataCollector\\EventDataCollector","file":"/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/Profiler/Profiler.php","line":115,"args":[]},{"function":"saveProfile","type":"->","class":"Symfony\\Component\\HttpKernel\\Profiler\\Profiler","file":"/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/EventListener/ProfilerListener.php","line":146,"args":[]},{"function":"onKernelTerminate","type":"->","class":"Symfony\\Component\\HttpKernel\\EventListener\\ProfilerListener","file":"/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/Debug/WrappedListener.php","line":61,"args":[]},{"function":"call_user_func:{/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/Debug/WrappedListener.php:61}","file":"/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/Debug/WrappedListener.php","line":61,"args":[]},{"function":"__invoke","type":"->","class":"Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener","file":"/var/www/html/phoenix/app/cache/dev/classes.php","line":1824,"args":[]},{"function":"call_user_func:{/var/www/html/phoenix/app/cache/dev/classes.php:1824}","file":"/var/www/html/phoenix/app/cache/dev/classes.php","line":1824,"args":[]},{"function":"doDispatch","type":"->","class":"Symfony\\Component\\EventDispatcher\\EventDispatcher","file":"/var/www/html/phoenix/app/cache/dev/classes.php","line":1757,"args":[]},{"function":"dispatch","type":"->","class":"Symfony\\Component\\EventDispatcher\\EventDispatcher","file":"/var/www/html/phoenix/app/cache/dev/classes.php","line":1918,"args":[]},{"function":"dispatch","type":"->","class":"Symfony\\Component\\EventDispatcher\\ContainerAwareEventDispatcher","file":"/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php","line":124,"args":[]},{"function":"dispatch","type":"->","class":"Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcher","file":"/var/www/html/phoenix/app/bootstrap.php.cache","line":3067,"args":[]},{"function":"terminate","type":"->","class":"Symfony\\Component\\HttpKernel\\HttpKernel","file":"/var/www/html/phoenix/app/bootstrap.php.cache","line":2409,"args":[]},{"function":"terminate","type":"->","class":"Symfony\\Component\\HttpKernel\\Kernel","file":"/var/www/html/phoenix/web/app_dev.php","line":20,"args":[]},{"function":"{main}","file":"/var/www/html/phoenix/web/app_dev.php","line":0,"args":[]}]} []
Is it possible to make the security.current_user_credentials and security.current_user optional? Is this error caused by these services?
Recently I ran into a similar issue and if you try to access a route that does not exist you might see the same error. I was working on a task where I needed to get hold of logged in user in my service and this is how I achieved it
My services.yml
services:
student_application_subscriber:
class: namespace\YourBundle\EventListener\StudentApplicationSubscriber
arguments:
- #doctrine.orm.entity_manager
- #security.token_storage
- #security.authorization_checker
- #twig
This is my service class StudentApplicationSubscriber
namespace yournamespace\YourBundleBundle\EventListener;
use Doctrine\ORM\EntityManager;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class StudentApplicationSubscriber implements EventSubscriberInterface
{
protected $em;
protected $twig;
protected $tokenStorage;
protected $authChecker;
function __construct(EntityManager $em, $tokenStorage, $authChecker, $twig)
{
$this->em = $em;
$this->twig = $twig;
$this->tokenStorage = $tokenStorage;
$this->authChecker = $authChecker;
}
public static function getSubscribedEvents()
{
return array(
'kernel.request' => 'onKernelRequest'
);
}
public function onKernelRequest()
{
if (!$token = $this->tokenStorage->getToken()) {
return;
}
$user = $token->getUser();
if (!is_object($user)) {
// there is no user - the user may not be logged in
return;
}
//get details of logged in user
$get_user_details = $this->tokenStorage->getToken()->getUser();
//make sure to pull information when user is logged in
if ($this->authChecker->isGranted('IS_AUTHENTICATED_FULLY')) {
//get user id of logged in user
$userId = $get_user_details->getId();
//perform your logic here
}
}
}
What are you trying to achieve?
At first sight I would suggest would be either having a kernel listener that would check if there is a User and performe the required actions, or check that in your security.current_user_credentials.
I guess, if you just pass to your service and add below logic inside that function, then it work for both annon and authenticated users:
function dummyFunction($securityContext)) {
$email = $username = '';
if($securityContext->isGranted('IS_AUTHENTICATED_FULLY')) {
$email = $securityContext->getToken()->getUser()->getEmail();
$username = $securityContext->getToken()->getUser()->getUsername();
}
..........................
}
I need to perform a set of actions after a user successfully logs in. This includes loading data from the database and storing it in the session.
What is the best approach to implementing this?
You can add a listener to the security.interactive_login event.
attach your listener like so. In this example I also pass the security context and session as dependencies.
Note: SecurityContext is deprecated as of Symfony 2.6. Please refer to
http://symfony.com/blog/new-in-symfony-2-6-security-component-improvements
parameters:
# ...
account.security_listener.class: Company\AccountBundle\Listener\SecurityListener
services:
# ...
account.security_listener:
class: %account.security_listener.class%
arguments: ['#security.context', '#session']
tags:
- { name: kernel.event_listener, event: security.interactive_login, method: onSecurityInteractiveLogin }
and in your listener you can store whatever you want on the session. In this case I set the users timezone.
<?php
namespace Company\AccountBundle\Listener;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
class SecurityListener
{
public function __construct(SecurityContextInterface $security, Session $session)
{
$this->security = $security;
$this->session = $session;
}
public function onSecurityInteractiveLogin(InteractiveLoginEvent $event)
{
$timezone = $this->security->getToken()->getUser()->getTimezone();
if (empty($timezone)) {
$timezone = 'UTC';
}
$this->session->set('timezone', $timezone);
}
}
You can even fetch the user instance from the event itself, no need to inject the token storage!
public function onSecurityInteractiveLogin(InteractiveLoginEvent $event)
{
$event->getAuthenticationToken()->getUser()
}
Im seraching over and cannot find answer.
I have database role model in my application. User can have a role but this role must be stored into database.
But then user needs to have default role added from database. So i created a service:
<?php
namespace Alef\UserBundle\Service;
use Alef\UserBundle\Entity\Role;
/**
* Description of RoleService
*
* #author oracle
*/
class RoleService {
const ENTITY_NAME = 'AlefUserBundle:Role';
private $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function findAll()
{
return $this->em->getRepository(self::ENTITY_NAME)->findAll();
}
public function create(User $user)
{
// possibly validation here
$this->em->persist($user);
$this->em->flush($user);
}
public function addRole($name, $role) {
if (($newrole = findRoleByRole($role)) != null)
return $newrole;
if (($newrole = findRoleByName($name)) != null)
return $newrole;
//there is no existing role
$newrole = new Role();
$newrole->setName($name);
$newrole->setRole($role);
$em->persist($newrole);
$em->flush();
return $newrole;
}
public function getRoleByName($name) {
return $this->em->getRepository(self::ENTITY_NAME)->findBy(array('name' => $name));
}
public function getRoleByRole($role) {
return $this->em->getRepository(self::ENTITY_NAME)->findBy(array('role' => $role));
}
}
my services.yml is:
alef.role_service:
class: Alef\UserBundle\Service\RoleService
arguments: [%doctrine.orm.entity_manager%]
And now I want to use it in two places:
UserController and User entity. How can i get them inside entity?
As for controller i think i just need to:
$this->get('alef.role_service');
But how to get service inside entity?
You don't. This is a very common question. Entities should only know about other entities and not about the entity manager or other high level services. It can be a bit of a challenge to make the transition to this way of developing but it's usually worth it.
What you want to do is to load the role when you load the user. Typically you will end up with a UserProvider which does this sort of thing. Have you read through the sections on security? That should be your starting point:
http://symfony.com/doc/current/book/security.html
The reason why it's so difficult to get services into entities in the first place is that Symfony was explicitly designed with the intent that services should never be used inside entities. Therefore, the best practice answer is to redesign your application to not need to use services in entities.
However, I have found there is a way to do it that does not involve messing with the global kernel.
Doctrine entities have lifeCycle events which you can hook an event listener to, see http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#lifecycle-events For the sake of the example, I'll use postLoad, which triggers soon after the Entity is created.
EventListeners can be made as services which you inject other services into.
Add to app/config/config.yml:
services:
example.listener:
class: Alef\UserBundle\EventListener\ExampleListener
arguments:
- '#alef.role_service'
tags:
- { name: doctrine.event_listener, event: postLoad }
Add to your Entity:
use Alef\UserBundle\Service\RoleService;
private $roleService;
public function setRoleService(RoleService $roleService) {
$this->roleService = $roleService;
}
And add the new EventListener:
namespace Alef\UserBundle\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Alef\UserBundle\Service\RoleService;
class ExampleListener
{
private $roleService;
public function __construct(RoleService $roleService) {
$this->roleService = $roleService;
}
public function postLoad(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if(method_exists($entity, 'setRoleService')) {
$entity->setRoleService($this->roleService);
}
}
}
Just keep in mind this solution comes with the caveat that this is still the quick and dirty way, and really you should consider redesigning your application the proper way.
Thanks to Kai's answer above which answer to the question, but it's not compatible with symfony 5.x .
It's good to precise it's a bad practice, but required in some special case like legacy code or a bad DB design (as a temporary solution before schema migration)
As in my case, I use this code with a mailer and translator, which introduce an issue with the private property if Symfony >= 5.3 , so here the solution for recent version of symfony:
in config/services.yaml:
services:
Alef\UserBundle\EventListener\ExampleListener:
tags:
- { name: doctrine.event_listener, event: postLoad }
ExampleListener:
namespace Alef\UserBundle\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Alef\UserBundle\Entity\Role;
class ExampleListener
{
public function postLoad(LifecycleEventArgs $postLoad): void
{
$entity = $postLoad->getEntity();
if ($entity instanceof User) {
$repository = ;
$entity->roleRepository(
$postLoad->getEntityManager()->getRepository(Role::class)
);
}
}
}
And in your Entity (or in a trait if you use it in more than one entity):
use Alef\UserBundle\Service\RoleService;
/** #internal bridge for legacy schema */
public function roleRepository(?RoleRepository $repository = null) {
static $roleRepository;
if (null !== $repository) {
$roleRepository = $repository;
}
return $roleRepository;
}
public function getRoleByName($name) {
return $this->roleRepository()->findBy(array('name' => $name));
}