What I want to do is show logged user only content that he has access to.
First thing I was doing access_control in security.yml & redirect but i must do at least 30 deferent accounts;/
Next i create twig extension that will connect to DB and get current logged user specific settings -
access to panels. Is this good way?
The problem is
$user = $this->getUser()->getId();
$currentUser = $this->em->getRepository('AppBundle:User')->find($user);
It will not work, Blank page appears in dev env
But when i put 1
$currentUser = $this->em->getRepository('AppBundle:User')->find(1);
& 1 is user id everything is ok.
services.yml
app.twig.users_extension:
class: AppBundle\Twig\Extension\AccesExtension
arguments: ["#doctrine.orm.entity_manager","#security.token_storage"]
tags:
- { name: twig.extension }
Twig Extension
class AccesExtension extends \Twig_Extension
{
protected $em;
protected $tokenStorage;
public function __construct(EntityManager $em, TokenStorage $tokenStorage)
{
$this->em = $em;
$this->tokenStorage = $tokenStorage;
}
public function getUser()
{
return $this->tokenStorage->getToken()->getUser();
}
public function getGlobals()
{
$user = $this->getUser()->getId();
$currentUser = $this->em->getRepository('AppBundle:User')->find($user);
return array (
"acces" => $currentUser,
);
}
public function getName()
{
return "AppBundle:AccesExtension ";
}
}
As people in the comments already told you, you can use security voters.
Here is a simple tutorial with video/text that explains the use of voters and also how to use them in twig, this should help you with the problem you have.
Related
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
It's possibile with security file config to redirect user already logged in to specific route (e.g homepage) if they try to access on login/register pages? One solution that I already found is to attach a listener to EventRequest, but I prefer to use security config if it's possible.
After some googling I noticed that another solution is to override the fosuserbundle controllers. But because I need that this behavior should works also for /register and /resetting pages, instead to override also those controller, I preferred to use EventListener. Maybe this's the best solution in this case. I'm using Symfony 4, so for the other versions could be different.
My code:
namespace App\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
class LoggedInUserListener
{
private $router;
private $authChecker;
public function __construct(RouterInterface $router, AuthorizationCheckerInterface $authChecker)
{
$this->router = $router;
$this->authChecker = $authChecker;
}
/**
* Redirect user to homepage if tryes to access in anonymously path
* #param GetResponseEvent $event
*/
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
$path = $request->getPathInfo();
if ($this->authChecker->isGranted('IS_AUTHENTICATED_REMEMBERED') && $this->isAnonymouslyPath($path)) {
$response = new RedirectResponse($this->router->generate('homepage'));
$event->setResponse($response);
}
}
/**
* Check if $path is an anonymously path
* #param string $path
* #return bool
*/
private function isAnonymouslyPath(string $path): bool
{
return preg_match('/\/login|\/register|\/resetting/', $path) ? true : false;
}
}
add this to services.yaml:
App\EventListener\LoggedInUserListener:
tags:
- { name: kernel.event_listener, event: kernel.request }
#Mintendo, I have errors using your code:
request.CRITICAL: Exception thrown when handling an exception (Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException: The token storage contains no authentication token.
php.CRITICAL: Uncaught Exception: The token storage contains no authentication token. One possible reason may be that there is no firewall configured for this URL.
Besides that debug bar also showed error and was broken.
But you pushed me in the right direction, so I have modified your code a little.
And it works without errors now:
<?php
namespace App\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Security;
class LoggedInUserListener
{
private $router;
private $security;
public function __construct(RouterInterface $router, Security $security)
{
$this->router = $router;
$this->security = $security;
}
/**
* Redirect user to homepage if tries to access in anonymously path
* #param GetResponseEvent $event
*/
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
$path = $request->getPathInfo();
if ($this->security->getUser() && $this->isAnonymouslyPath($path)) {
$response = new RedirectResponse($this->router->generate('dashboard'));
$event->setResponse($response);
}
}
/**
* Check if $path is an anonymously path
* #param string $path
* #return bool
*/
private function isAnonymouslyPath(string $path): bool
{
return preg_match('/\/login|\/register|\/resetting/', $path) ? true : false;
}
}
Question: How to get the form_login.check_path by given firewall name?
We subscribe to Symfony\Component\Security\Http\SecurityEvent::INTERACTIVE_LOGIN in order to log successful logins inside an Application that has multiple firewalls.
One firewall uses JWT tokens via Guard authentication which has the negative effect that this event is triggered for every request with a valid token.
We have currently solved this by manually checking whether the current route matches the firewall's check-path and stopping the event-propagation together with an early return otherwise.
As we're adding more firewalls (with different tokens) I'd like to solve this more generally. Therefore I want to check whether the current route matches the current firewalls check-path without hardcoding any route or firewall-name.
There is a class to generate Logout URLs for the current firewall used by Twig logout_path() method which gets the logout route/path from the firewall listeners somehow. (Symfony\Component\Security\Http\Logout\LogoutUrlGenerator)
Before I hop into a long debugging session I thought maybe someone has solved this case before ;)
Any ideas?
Example code:
class UserEventSubscriber implements EventSubscriberInterface
{
/** #var LoggerInterface */
protected $logger;
/** #var FirewallMapInterface|FirewallMap */
protected $firewallMap;
public function __construct(LoggerInterface $logger, FirewallMapInterface $firewallMap)
{
$this->logger = $logger;
$this->firewallMap = $firewallMap;
}
public function onInteractiveLogin(InteractiveLoginEvent $event)
{
$request = $event->getRequest();
$firewallName = $this->firewallMap->getFirewallConfig($request)->getName();
$routeName = $request->get('_route');
if (('firewall_jwt' === $firewallName) && ('firewall_jwt_login_check' !== $routeName)) {
$event->stopPropagation();
return;
}
$this->logger->info(
'A User has logged in interactively.',
array(
'event' => SecurityEvents::INTERACTIVE_LOGIN,
'user' => $event->getAuthenticationToken()->getUser()->getUuid(),
));
The check_path option is only available from authentication factory/listener, so you could pass this configuration manually to the subscriber class while the container is building.
This solution take account that check_path could be a route name or path, that's why HttpUtils service is injected too:
namespace AppBundle\Subscriber;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\FirewallMapInterface;
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Component\Security\Http\SecurityEvents;
class UserEventSubscriber implements EventSubscriberInterface
{
private $logger;
private $httpUtils;
private $firewallMap;
private $checkPathsPerFirewall;
public function __construct(LoggerInterface $logger, HttpUtils $httpUtils, FirewallMapInterface $firewallMap, array $checkPathsPerFirewall)
{
$this->logger = $logger;
$this->httpUtils = $httpUtils;
$this->firewallMap = $firewallMap;
$this->checkPathsPerFirewall = $checkPathsPerFirewall;
}
public function onInteractiveLogin(InteractiveLoginEvent $event)
{
$request = $event->getRequest();
$firewallName = $this->firewallMap->getFirewallConfig($request)->getName();
$checkPath = $this->checkPathsPerFirewall[$firewallName];
if (!$this->httpUtils->checkRequestPath($request, $checkPath)) {
$event->stopPropagation();
return;
}
$this->logger->info('A User has logged in interactively.', array(
'event' => SecurityEvents::INTERACTIVE_LOGIN,
'user' => $event->getAuthenticationToken()->getUser()->getUsername(),
));
}
public static function getSubscribedEvents()
{
return [SecurityEvents::INTERACTIVE_LOGIN => 'onInteractiveLogin'];
}
}
After regiter this subscriber as service (AppBundle\Subscriber\UserEventSubscriber) we need implement PrependExtensionInterface in your DI extension to be able to access the security configuration and complete the subscriber definition with the check paths per firewall:
namespace AppBundle\DependencyInjection;
use AppBundle\Subscriber\UserEventSubscriber;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
class AppExtension extends Extension implements PrependExtensionInterface
{
// ...
public function prepend(ContainerBuilder $container)
{
$checkPathsPerFirewall = [];
$securityConfig = $container->getExtensionConfig('security');
foreach ($securityConfig[0]['firewalls'] as $name => $config) {
if (isset($config['security']) && false === $config['security']) {
continue; // skip firewalls without security
}
$checkPathsPerFirewall[$name] = isset($config['form_login']['check_path'])
? $config['form_login']['check_path']
: '/login_check'; // default one in Symfony
}
$subscriber = $container->getDefinition(UserEventSubscriber::class);
$subscriber->setArgument(3, $checkPathsPerFirewall);
}
}
I hope it fits your need.
for PHP8
In __construct :
public function __construct(
private RequestStack $requestStack,
private FirewallMapInterface $firewallMap
)
{
}
use this :
$firewallName = $this->firewallMap->getFirewallConfig($this->requestStack->getCurrentRequest())->getName();
I'm in a symfony 2.8 project. Another developer wrote a Voter to check if a user has the permissions or not for a specific task. I already use this function in a controller without problems but now I'm writing a service to get a dynamic menu and I don't know how to access to the method isGranted:
Error: Attempted to call an undefined method named "isGranted" of
class.
namespace NameSpaceQuestion\Question\Service;
use Doctrine\ORM\EntityManager;
class MenuBuilder
{
private $areasTools;
public $menuItems = array();
private $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function createMenuCourse($course,$mode,$user)
{
$repository = $this->em->getRepository('eBundle:AreasTools');
$areas = $repository->findAll();
//Retrieve every area from the db
$itemsMenu = array();
foreach ($areas as $area) {
//If the user has permissions the area is included in the menu and proceed to check the tools that belong to the current area
if($this->isGranted( $mode,$area,$user ) ){
$itemsMenu[$area->getName()] = array();
$toolsPerCourse = $this->em->getRepository('eBundle:CourseTool')->findByAreaToolAndCourse($area, $course);
foreach ($toolsPerCourse as $toolCourse) {
//If the user has permissions the tool is included in the menu under the respective Area
if( ($this->isGranted( $mode,$toolCourse,$user ) )){
array_push($itemsMenu[$area->getName()], $toolCourse->getName());
}
}
}
}
return $itemsMenu;
}
}
You'll have to use Dependency Injection to get the AuthorizationChecker in your MenuBuilder class. You can read about it here: http://symfony.com/doc/current/cookbook/security/securing_services.html
Because it looks like you're already injecting the EntityManager, just add the AuthorizationChecker to your code:
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
class MenuBuilder
{
protected $authorizationChecker;
public function __construct(EntityManager $em, AuthorizationCheckerInterface $authorizationChecker)
{
$this->authorizationChecker = $authorizationChecker;
$this->em = $em;
}
function createMenuCourse()
{
if ( $this->authorizationChecker->isGranted('EDIT',$user) ) {
//build menu
}
}
}
First thing to understand is that the Symfony base controller has a number of helper functions such as isGranted implemented. But of course, if you are not inside of a controller then you don't have access to them. On the other hand, it's instructive to look at the base class and copy out the needed functionality.
The isGranted functionality relies on a authorization checker service which you will need to inject into your menu builder resulting in something like:
class MenuBuilder
{
private $em;
private $authorizationChecker;
public function __construct(
EntityManager $em,
$authorizationChecker // security.authorization_checker
) {
$this->em = $em;
$this->authorizationChecker = $authorizationChecker;
}
protected function isGranted($attributes, $object = null)
{
return $this->authorizationChecker->isGranted($attributes, $object);
}
Darn it. Stephan beat me by one minute. Must learn to type faster.
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;
}
}