I have a website made with Symfony 3.4 and within my actions I must check if the current user can edit the target product, something like this:
/**
* #Route("/products/{id}/edit")
*/
public function editAction(Request $request, Product $product)
{
// security
$user = $this->getUser();
if ($user != $product->getUser()) {
throw $this->createAccessDeniedException();
}
// ...
}
How can I avoid making the same check on every action (bonus points if using annotations and expressions)?
I am already using security.yml with access_control to deny access based on roles.
You can use Voters for this exact purpose. No magic involved. After creating and registering the Voter authentication will be done automatically in the security layer.
You just have to create the Voter class and then register it as a service. But if you're using the default services.yaml configuration, registering it as a service is done automatically for you!
Here is an example you can use. You may have to change a few items but this is basically it.
To read more visit: https://symfony.com/doc/current/security/voters.html
<?php
namespace AppBundle\Security;
use AppBundle\Entity\Product;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use AppBundle\Entity\User;
class ProductVoter extends Voter
{
const EDIT = 'EDIT_USER_PRODUCT';
protected function supports($attribute, $subject)
{
if($attribute !== self::EDIT) {
return false;
}
if(!$subject instanceof Product) {
return false;
}
return true;
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
/** #var Product $product */
$product= $subject;
$user = $token->getUser();
if (!$user instanceof User) {
// the user must be logged in; if not, deny access
return false;
}
return $this->belongsToUser($product, $user);
}
private function belongsToUser(Product $product, User $user)
{
return $user->getId() === $product->getUser()->getId();
}
}
You could try with a listener:
Check the action name,for example, if it is "edit_product", them continue.
Get the current logged User.
Get the user of the product entity.
Check if current user is different to Product user, if it is true, throw CreateAccessDeniedException.
services.yml
app.user.listener:
class: AppBundle\EventListener\ValidateUserListener
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
arguments: ["#service_container", "#doctrine.orm.entity_manager"]
Edit Action:
Added name "edit_product" to the action.
/**
*
* #Route("/products/{id}/edit",name="edit_product")
*/
public function editAction()
{
...
src\AppBundle\EventListener\ValidateUserListener.php
<?php
namespace AppBundle\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class ValidateUserListener
{
private $container;
private $entityManager;
public function __construct($container, $entityManager)
{
$this->container = $container;
$this->entityManager = $entityManager;
}
public function onKernelRequest(GetResponseEvent $event)
{
$currentRoute = $event->getRequest()->attributes->get('_route');
if($currentRoute=='edit_product' || $currentRoute=='edit_item' )
{
$array_user = $this->getCurrentUser();
if($array_user['is_auth'])
{
$current_user = $array_user['current_user'];
$product = $this->entityManager->getRepository('AppBundle:User')->findOneByUsername($current_user);
$product_user = $product->getUsername();
if ($current_user !==$product_user)
{
throw $this->createAccessDeniedException();
}
}
}
}
private function getCurrentUser()
{
//Get the current logged User
$user = $this->container->get('security.token_storage')->getToken()->getUser();
if(null!=$user)
{
//If user is authenticated
$isauth = $this->container->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY');
return array('is_auth'=>$isauth, 'current_user'=>$user);
}
return array('is_auth'=>false, 'current_user'=>$user);
}
}
Tested in Symfony 3.3
Related
My security user is fully authenticated with some roles got from some system. I want to check if one of the Roles exists and if it does not, I want to force de-authentication of the user.
In my event listener on the login I do this :
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
class SecurityEventSubscriber implements EventSubscriberInterface {
private $token;
private $checker;
private $container;
private $session;
public function __construct(TokenStorageInterface $token, AuthorizationCheckerInterface $checker, ContainerInterface $container, SessionInterface $session) {
$this->token = $token;
$this->checker = $checker;
$this->container = $container;
$this->session = $session;
}
public function login() {
if(!$this->checker->isGranted('IS_AUTHENTICATED_FULLY')) {
$this->session->invalidate();
$this->token->setToken(null);
throw new AccessDeniedException();
} else {
$user = $this->token->getToken()->getUser();
$roles = $user->getRoles();
$found = false;
foreach ($roles as $role) {
if($role->getRole() === $this->container->getParameter('role_expected')) {
$found = true;
break;
}
}
if(!$found) {
$this->session->invalidate();
$this->token->setToken(null);
throw new AccessDeniedException();
} else {
$user->removeAllRoles();
}
}
}
}
As you can see I tried to use the setToken to null but it does not work (Exception).
HGow should I ask to de-authenticate the user ?
Instead of checking the user permissions in your controller, you could create a custom User Checker that could deny the authentication based on your custom logic.
Sample User Checker
namespace AppBundle\Security;
use AppBundle\Security\User as AppUser;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class UserChecker implements UserCheckerInterface
{
public function checkPreAuth(UserInterface $user)
{
if (!$user instanceof AppUser) {
return;
}
if (!in_array('SOME_ROLE', $user->getRoles())) {
// throw an AccountStatusException exception here
}
}
}
If you also want to run a check against the user roles after user has been logged in (if his roles could change during his session) you can use the checkPostAuth() method.
You also have to mention the use of your custom User Checker in your app/config/security.yml file.
security:
firewalls:
main:
pattern: ^/
user_checker: AppBundle\Security\UserChecker
More informations here
The easiest way is to redirect your user to your logout route.
Sadly there does not seem to be a dedicated method that will simply handle the whole logout process for you.
Except maybe if you manage to call this method in a valid instance of Symfony\Component\Security\Http\Firewall\LogoutListener.
Symfony logout works like that:
It is handled by this listener
Listener is called on each request
Listener checks if requested route === logout route
If requested route === logout route, listener logs out the user
I am working on a Symfony 3.3.8 project with FOSUserBundle. I have created two registration and two profile pages for student and provider respectively. Now I am trying to override the FOSUserBundle's loginAction() method, where I am checking the logged-in user for it's role. If the role is ROLE_STUDENT I am trying to redirect the user to student profile page after successful login and if the role is ROLE_PROVIDER I want the user to be redirected to the provider profile page after successful login. Here is my overridden SecurityController with loginAction():
<?php
namespace Epita\HousingBundle\Controller;
use FOS\UserBundle\Controller\SecurityController as BaseController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
class SecurityController extends BaseController {
/**
*
*/
public function loginAction(Request $request) {
/** #var $session \Symfony\Component\HttpFoundation\Session\Session */
$session = $request->getSession();
$key = '_security.main.target_path';
$securityContext = $this->container->get('security.authorization_checker');
if ($securityContext->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
$user = $this->getUser();
}
if ($this->container->get('session')->has($key))
{
if ($this->get('security.authorization_checker')->isGranted('ROLE_STUDENT')) {
return $this->redirectToRoute('student_profile');
} else if ($this->get('security.authorization_checker')->isGranted('ROLE_PROVIDER')) {
return $this->redirectToRoute('provider_profile');
}
}
else{
return $this->redirectToRoute('student_profile');
}
if (class_exists('\Symfony\Component\Security\Core\Security')) {
$authErrorKey = Security::AUTHENTICATION_ERROR;
$lastUsernameKey = Security::LAST_USERNAME;
} else {
// BC for SF < 2.6
$authErrorKey = SecurityContextInterface::AUTHENTICATION_ERROR;
$lastUsernameKey = SecurityContextInterface::LAST_USERNAME;
}
// get the error if any (works with forward and redirect -- see below)
if ($request->attributes->has($authErrorKey)) {
$error = $request->attributes->get($authErrorKey);
} elseif (null !== $session && $session->has($authErrorKey)) {
$error = $session->get($authErrorKey);
$session->remove($authErrorKey);
} else {
$error = null;
}
if (!$error instanceof AuthenticationException) {
$error = null; // The value does not come from the security component.
}
// last username entered by the user
$lastUsername = (null === $session) ? '' : $session->get($lastUsernameKey);
if ($this->has('security.csrf.token_manager')) {
$csrfToken = $this->get('security.csrf.token_manager')->getToken('authenticate')->getValue();
} else {
// BC for SF < 2.4
$csrfToken = $this->has('form.csrf_provider')
? $this->get('form.csrf_provider')->generateCsrfToken('authenticate')
: null;
}
return $this->renderLogin(array(
'last_username' => $lastUsername,
'error' => $error,
'csrf_token' => $csrfToken,
));
}
/**
*
* #param array $data
*
* #return \Symfony\Component\HttpFoundation\Response
*/
protected function renderLogin(array $data)
{
$template = sprintf('EpitaHousingBundle:Security:login.html.twig');
return $this->container->get('templating')->renderResponse($template, $data);
}
public function checkAction()
{
throw new \RuntimeException('You must configure the check path to be handled by the firewall using form_login in your security firewall configuration.');
}
public function logoutAction()
{
throw new \RuntimeException('You must activate the logout in your security firewall configuration.');
}
}
But this solution does not work for me. My guess is I have to play around with the session. Since I am new to this can anyone help me?
Thanks in advance. Let me know if you need more details.
Here is some code that might help you.
1 - This is the Event/LoginSuccessHandler, your logic is in this class
<?php
namespace AppBundle\Event;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Router;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationChecker;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
class LoginSuccessHandler implements AuthenticationSuccessHandlerInterface
{
protected $router;
protected $security;
public function __construct(Router $router, AuthorizationChecker $security)
{
$this->router = $router;
$this->security = $security;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token)
{
$url = 'homepage';
if ($this->security->isGranted('ROLE_STUDENT')) {
$url = 'student_route';
}
if ($this->security->isGranted('ROLE_PROVIDER')) {
$url = 'provider_route';
}
$response = new RedirectResponse($this->router->generate($url));
return $response;
}
}
2 - We set up the config that listens the login event in your services.yml
login_success_handler:
class: AppBundle\Event\LoginSuccessHandler
arguments:
- "#router"
- "#security.authorization_checker"
tags:
- { name: 'monolog.logger', channel: 'security' }
3 - That should be it. Try and tell me if something is wrong
I'm trying to implement a custom Voter.
From the controller I call it this way:
$prj = $this->getDoctrine()->getRepository('AppBundle:Project')->findOneById($id);
if (false === $this->get('security.authorization_checker')->isGranted('responsible', $prj)) {
throw new AccessDeniedException('Unauthorised access!');
}
The first line properly retrieves the Project object (I checked with a dump).
The problem occurs inside the voter
<?php
namespace AppBundle\Security\Authorization\Voter;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class ProjectVoter implements VoterInterface
{
const RESPONSIBLE = 'responsible';
const ACCOUNTABLE = 'accountable';
const SUPPORT = 'support';
const CONSULTED = 'consulted';
const INFORMED = 'informed';
public function supportsAttribute($attribute)
{
return in_array($attribute, array(
self::RESPONSIBLE,
self::ACCOUNTABLE,
self::SUPPORT,
self::CONSULTED,
self::INFORMED,
));
}
public function supportsClass($class)
{
$supportedClass = 'AppBundle\Entity\Project';
return $supportedClass === $class || is_subclass_of($class, $supportedClass);
}
/**
* #var \AppBundle\Entity\Project $project
*/
public function vote(TokenInterface $token, $project, array $attributes)
{
// check if class of this object is supported by this voter
if (!$this->supportsClass(get_class($project))) {
return VoterInterface::ACCESS_ABSTAIN;
}
// check if the voter is used correct, only allow one attribute
// this isn't a requirement, it's just one easy way for you to
// design your voter
if (1 !== count($attributes)) {
throw new \InvalidArgumentException(
'Only one attribute is allowed'
); //in origin it was 'for VIEW or EDIT, which were the supported attributes
}
// set the attribute to check against
$attribute = $attributes[0];
// check if the given attribute is covered by this voter
if (!$this->supportsAttribute($attribute)) {
return VoterInterface::ACCESS_ABSTAIN;
}
// get current logged in user
$user = $token->getUser();
// make sure there is a user object (i.e. that the user is logged in)
if (!$user instanceof UserInterface) {
return VoterInterface::ACCESS_DENIED;
}
$em = $this->getDoctrine()->getManager();
$projects = $em->getRepository('AppBundle:Project')->findPrjByUserAndRole($user, $attribute);
foreach ($projects as $key => $prj) {
if ($prj['id'] === $project['id'])
{
$granted = true;
$index = $key; // save the index of the last time a specifif project changed status
}
}
if($projects[$index]['is_active']===true) //if the last status is active
return VoterInterface::ACCESS_GRANTED;
else
return VoterInterface::ACCESS_DENIED;
}
}
I get the following error
Attempted to call method "getDoctrine" on class
"AppBundle\Security\Authorization\Voter\ProjectVoter".
I understand that the controller extends Controller, that is why I can use "getDoctrine" there. How can I have access to my DB from inside the Voter?
I solved it. This is pretty curious: I spend hours or days on a problem, then post a question here, and I solve it myself within an hour :/
I needed to add the following in my voter class:
public function __construct(EntityManager $em)
{
$this->em = $em;
}
I needed to add the following on top:
use Doctrine\ORM\EntityManager;
I also needed to add the arguments in the service.yml
security.access.project_voter:
class: AppBundle\Security\Authorization\Voter\ProjectVoter
arguments: [ #doctrine.orm.entity_manager ]
public: false
tags:
- { name: security.voter }
I want to store the last locale used by a user on every Request the user makes. I already created a Field in the Database and only need the best practice way without big modifications in every Controller.
Thank you all.
You could store your locale in session and use it with an event listener. And on user login set user locale in session.
-LocaleListener : Store the locale in session and user locale in session
-UserLocaleListener : On user login set user locale in session
<?php
namespace YourApp\YourBundle\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class LocaleListener implements EventSubscriberInterface
{
private $defaultLocale;
public function __construct($defaultLocale = 'en')
{
$this->defaultLocale = $defaultLocale;
}
public function onKernelRequest17(GetResponseEvent $event)
{
$request = $event->getRequest();
if (!$request->hasPreviousSession()) {
return;
}
// try to see if the locale has been set as a _locale routing parameter
if ($locale = $request->attributes->get('_locale')) {
$request->getSession()->set('_locale', $locale);
} else {
// if no explicit locale has been set on this request, use one from the session
$request->setLocale($request->getSession()->get('_locale', $this->defaultLocale));
}
}
public static function getSubscribedEvents()
{
return array(
// must be registered before the default Locale listener
KernelEvents::REQUEST => array(array('onKernelRequest17', 17)),
);
}
}
Second service:
<?php
namespace YourApp\YourBundle\EventListener;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
class UserLocaleListener
{
private $session;
public function setSession(Session $session)
{
$this->session = $session;
}
/**
* kernel.request event. If a guest user doesn't have an opened session, locale is equal to
* "undefined" as configured by default in parameters.ini. If so, set as a locale the user's
* preferred language.
*
* #param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
*/
public function setLocaleForUnauthenticatedUser(GetResponseEvent $event)
{
if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
return;
}
$request = $event->getRequest();
if ('undefined' == $request->getLocale()) {
if ($locale = $request->getSession()->get('_locale')) {
$request->setLocale($locale);
} else {
$request->setLocale($request->getPreferredLanguage());
}
}
}
/**
* security.interactive_login event. If a user chose a language in preferences, it would be set,
* if not, a locale that was set by setLocaleForUnauthenticatedUser remains.
*
* #param \Symfony\Component\Security\Http\Event\InteractiveLoginEvent $event
*/
public function setLocaleForAuthenticatedUser(InteractiveLoginEvent $event)
{
$user = $event->getAuthenticationToken()->getUser();
if ($lang = $user->getLocale()) {
$event->getRequest()->setLocale($lang);
$this->session->set('_locale', $lang);
}
}
}
services.yml
services:
yourapp_your.locale_listener:
class: YourApp\YourBundle\EventListener\LocaleListener
arguments: ["%kernel.default_locale%"]
tags:
- { name: kernel.event_subscriber }
yourapp_your.locale.interactive_login_listener:
class: YourApp\YourBundle\EventListener\UserLocaleListener
calls:
- [ setSession, [#session] ]
tags:
- { name: kernel.event_listener, event: security.interactive_login, method: setLocaleForAuthenticatedUser }
This is using
https://github.com/symfony/symfony/blob/master/UPGRADE-2.1.md#simulate-old-behavior
Translations in Symfony 2.3 locale in request
Symfony2 locale detection: not considering _locale in session
symfony 2 set locale based on user preferences stored in DB
That is my implementation to save the locale on each request in the database.
It is necessary for me because i need the locale persisted for the newsletter.
LocaleListener:
namespace MyApp\MyBundle\EventListener;
use \Symfony\Component\HttpKernel\Event\GetResponseEvent;
use \Symfony\Component\HttpKernel\KernelEvents;
use \Symfony\Component\EventDispatcher\EventSubscriberInterface;
use \FOS\UserBundle\Model\UserManagerInterface;
use \Symfony\Component\Security\Core\SecurityContext;
use \MyApp\MyBundle\Entity\User;
class LocaleListener implements EventSubscriberInterface
{
private $userManager;
private $securityContext;
private $defaultLocale;
public function __construct(UserManagerInterface $userManager, SecurityContext $securityContext, $defaultLocale = 'en')
{
$this->userManager = $userManager;
$this->securityContext = $securityContext;
$this->defaultLocale = $defaultLocale;
}
public function onKernelRequest17(GetResponseEvent $event)
{
$request = $event->getRequest();
if (!$request->hasPreviousSession()) {
return;
}
// try to see if the locale has been set as a _locale routing parameter
$locale = $request->attributes->get('_locale');
if ($locale !== null)
{
$request->getSession()->set('_locale', $locale);
}
else
{
// if no explicit locale has been set on this request, use one from the session
$locale = $request->getSession()->get('_locale', $this->defaultLocale);
$request->setLocale($locale);
}
// save last locale to the user if he is logged in
$user = $this->getUser();
if($user instanceof User)
{
$user->setDefaultLanguage($locale);
$this->userManager->updateUser($user);
}
}
private function getUser()
{
$token = $this->securityContext->getToken();
if($token !== null)
return $this->securityContext->getToken()->getUser();
}
public static function getSubscribedEvents()
{
return array(
// must be registered before the default Locale listener
KernelEvents::REQUEST => array(array('onKernelRequest17', 17)),
);
}
}
services.yml:
myApp_myBundle.locale_listener:
class: MyApp\MyBundle\EventListener\LocaleListener
arguments: [#fos_user.user_manager, #security.context, "%kernel.default_locale%"]
tags:
- { name: kernel.event_subscriber }
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;
}
}