I am using Synfony2 with FOSUserBundle and I have a custom userChecker where I want to validate the host of the user (we have several hosts pointing to the same IP). My problem is that inside my custom userChecker I can't access REQUEST, thus not the HOST of the request.
This is my user checker code
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien#symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
//Override by Mattias
namespace BizTV\UserBundle\Controller;
//namespace Symfony\Component\Security\Core\User;
use Symfony\Component\Security\Core\Exception\CredentialsExpiredException;
use Symfony\Component\Security\Core\Exception\LockedException;
use Symfony\Component\Security\Core\Exception\DisabledException;
use Symfony\Component\Security\Core\Exception\AccountExpiredException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserChecker as OriginalUserChecker;
use Symfony\Component\HttpFoundation\Request as Request; //ADDED BY MW
/**
* UserChecker checks the user account flags.
*
* #author Fabien Potencier <fabien#symfony.com>
*/
class UserCheckerNew extends OriginalUserChecker
{
/**
* {#inheritdoc}
*/
public function checkPreAuth(UserInterface $user)
{
/*
//Test for companylock...
if ( !$user->getCompany()->getActive() ) {
throw new LockedException('The company of this user is locked.', $user);
}
if ( $user->getLocked() ) {
throw new LockedException('The admin of this company has locked this user.', $user);
}
*/
if (!$user instanceof AdvancedUserInterface) {
return;
}
if (!$user->isCredentialsNonExpired()) {
throw new CredentialsExpiredException('User credentials have expired.', $user);
}
}
/**
* {#inheritdoc}
*/
public function checkPostAuth(UserInterface $user)
{
//Test for companylock...
if ( !$user->getCompany()->getActive() ) {
throw new LockedException('The company of this user is locked.');
}
if ( $user->getLocked() ) {
throw new LockedException('The admin of this company has locked this user.');
}
/*
Validate HOST here
*/
if (!$user instanceof AdvancedUserInterface) {
return;
}
if (!$user->isAccountNonLocked()) {
throw new LockedException('User account is locked.', $user);
}
if (!$user->isEnabled()) {
throw new DisabledException('User account is disabled.', $user);
}
if (!$user->isAccountNonExpired()) {
throw new AccountExpiredException('User account has expired.', $user);
}
}
}
In the checkPostAuth function I tried different things like passing the request
public function checkPostAuth(UserInterface $user, Request $request)
Error saying my override must conform to the original/interface.
Trying to get the request as in a controller
$this->container->get('request_stack')->getCurrentRequest();
or like this
$currentHost = $request->getHost();
or like this
$cont = $this->getContainer();
or like this
$request = $this->getRequest();
or like this
$request = $container->get('request');
Yet no luck =) I'm no Symfony2 guru, as you can tell, I'm shooting from the hip here =)
Added parameters to config.yml according to gp-sflover's answer, my config.yml now looks like this:
services:
security.user_checker:
class: BizTV\UserBundle\Controller\UserCheckerNew
arguments: [ "#request" ]
scope: request
public: true
The error delivered before scope:request was added to the config was:
Scope Widening Injection detected: The definition "security.user_checker" references the service "request" which belongs to a narrower scope. Generally, it is safer to either move "security.user_checker" to scope "request" or alternatively rely on the provider pattern by injecting the container itself, and requesting the service "request" each time it is needed. In rare, special cases however that might not be necessary, then you can set the reference to strict=false to get rid of this error.'
When adding scope: request a very similar error is returned
Scope Widening Injection detected: The definition "security.authentication.provider.dao.main" references the service "security.user_checker" which belongs to a narrower scope. Generally, it is safer to either move "security.authentication.provider.dao.main" to scope "request" or alternatively rely on the provider pattern by injecting the container itself, and requesting the service "security.user_checker" each time it is needed. In rare, special cases however that might not be necessary, then you can set the reference to strict=false to get rid of this error
Adding public: true doesn't seem to make a difference. Also, I don't know what this public stuff really means, perhaps a security issue? The word public is always scary =)
Instead of extend the "OriginalUserChecker" class you can override the security.user_checker service to be able to inject the request_stack as argument and then retrieve it in your UserChecker class like this simple example:
service.xml
// Symfony >=2.6
<service id="security.user_checker"
class="Your\Bundle\Path\ToYour\UserCheckerClass">
<argument type="service" id="request_stack"/>
</service>
// Symfony <2.6
<service id="security.user_checker"
class="Your\Bundle\Path\ToYour\UserCheckerClass">
<argument type="service" id="request" public="true" scope="request"/>
</service>
UserCheckerClass
use Symfony\Component\Security\Core\User\UserCheckerInterface;
// Symfony >=2.6
use Symfony\Component\HttpFoundation\RequestStack;
// Symfony <2.6
use Symfony\Component\HttpFoundation\Request;
class UserChecker implements UserCheckerInterface
{
private $request;
public function __construct(
// Symfony >=2.6
RequestStack $request
// Symfony <2.6
Request $request
) {
$this->request = $request;
}
public function checkPreAuth(UserInterface $user)
{
// your checks here
}
public function checkPostAuth(UserInterface $user)
{
// your checks here
}
}
I never did get the request injection to work. I did however get an injection of the entire service container to work.
This is how I ended up doing it, with guidance from gp_sflover (if you post an answer with this code I will check yours as the correct answer, I don't want to steal cred, just get the truth out there ;] )
services:
security.user_checker:
#class: BizTV\UserBundle\Controller\UserChecker
class: BizTV\UserBundle\Controller\UserCheckerNew
arguments: ["#service_container"]
namespace BizTV\UserBundle\Controller;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\CredentialsExpiredException;
use Symfony\Component\Security\Core\Exception\LockedException;
use Symfony\Component\Security\Core\Exception\DisabledException;
use Symfony\Component\Security\Core\Exception\AccountExpiredException;
class UserCheckerNew implements UserCheckerInterface
{
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function checkPreAuth(UserInterface $user)
{
if (!$user instanceof AdvancedUserInterface) {
return;
}
if (!$user->isCredentialsNonExpired()) {
throw new CredentialsExpiredException('User credentials have expired.', $user);
}
}
/**
* {#inheritdoc}
*/
public function checkPostAuth(UserInterface $user)
{
//Validate HOST here, make it look as though account doesn't exist if on wrong host
$host = $this->container->get('request')->getHost();
if ($host != "localhost") { //bypass all checks when on localhost
$brandedHost = $user->getCompany()->getBrandedHost();
if ( $brandedHost == "" ) { //if unset assume main
$brandedHost = "login.mydomain.se";
}
if ( $host != $brandedHost ) {
throw new LockedException('Invalid username or password.'); //dot added for debug
}
}
// end of host validation
//Test for companylock...
if ( !$user->getCompany()->getActive() ) {
throw new LockedException('The company of this user is locked.');
}
if ( $user->getLocked() ) {
throw new LockedException('The admin of this company has locked this user.');
}
if (!$user instanceof AdvancedUserInterface) {
return;
}
if (!$user->isAccountNonLocked()) {
throw new LockedException('User account is locked.', $user);
}
if (!$user->isEnabled()) {
throw new DisabledException('User account is disabled.', $user);
}
if (!$user->isAccountNonExpired()) {
throw new AccountExpiredException('User account has expired.', $user);
}
}
}
Related
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 have a user object that has a property 'enabled'. I want every action to first check if the user is enabled before continuing.
Right now I have solved it with a Controller that every other controller extends, but using the setContainer function to catch every Controller action feels really hacky.
class BaseController extends Controller{
public function setContainer(ContainerInterface $container = null)
{
$this->container = $container;
$user = $this->getUser();
// Redirect disabled users to a info page
if (!$user->isEnabled() && !$this instanceof InfoController) {
return $this->redirectToRoute('path_to_info');
}
}
I have tried building this using a before filter (http://symfony.com/doc/current/event_dispatcher/before_after_filters.html), but could not get the User object..any tips?
EDIT:
This is my solution:
namespace AppBundle\Security;
use AppBundle\Controller\AccessDeniedController;
use AppBundle\Controller\ConfirmController;
use Symfony\Bundle\FrameworkBundle\Routing\Router;
use Symfony\Bundle\TwigBundle\Controller\ExceptionController;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
class UserEnabledListener
{
private $tokenStorage;
private $router;
public function __construct(TokenStorage $tokenStorage, Router $router)
{
$this->tokenStorage = $tokenStorage;
$this->router = $router;
}
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
/*
* $controller passed can be either a class or a Closure.
* This is not usual in Symfony but it may happen.
* If it is a class, it comes in array format
*/
if (!is_array($controller)) {
return;
}
$controller = $controller[0];
// Skip enabled check when:
// - we are already are the AccessDenied controller, or
// - user confirms e-mail and becomes enabled again, or
// - Twig throws error in template
if ($controller instanceof AccessDeniedController ||
$controller instanceof ConfirmController ||
$controller instanceof ExceptionController) {
return;
}
$user = $this->tokenStorage->getToken()->getUser();
// Show info page when user is disabled
if (!$user->isEnabled()) {
$redirectUrl = $this->router->generate('warning');
$event->setController(function() use ($redirectUrl) {
return new RedirectResponse($redirectUrl);
});
}
}
}
EDIT 2:
Ok so turns out checking for each controller manually is really bad, as you will miss Controllers from third party dependencies. I'm going to use the Security annotation and do further custom logic in a custom Exception controller or template etc.
You can use an event listener to listen for any new request.
You'll need to inject the user and then do your verification:
<service id="my_request_listener" class="Namespace\MyListener">
<tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" />
<argument type="service" id="security.token_storage" />
</service>
Edit: Here is a snippet to give an example
class MyRequestListener {
private $tokenStorage;
public function __construct(TokenStorage $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public function onKernelRequest(GetResponseEvent $event)
{
if (!$event->getRequest()->isMasterRequest()) {
// don't do anything if it's not the master request
return;
}
if ($this->tokenStorage->getToken()) {
$user = $this->tokenStorage->getToken()->getUser();
//do your verification here
}
}
In your case I would use the #Security annotation, which can be very flexible if you use the expression language.
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
/**
* #Security("user.isEnabled()")
*/
class EventController extends Controller
{
// ...
}
In the end it's only 1 line in each of your controller files, and it has the advantage of being very readable (a developer new to the project would know immediately what is going on without having to go and check the contents of a BaseController or any potential before filter...)
More documentation on this here.
You can override also getuser() function in your BaseController also.
/**
* Get a user from the Security Token Storage.
*
* #return mixed
*
* #throws \LogicException If SecurityBundle is not available
*
* #see TokenInterface::getUser()
*/
protected function getUser()
{
if (!$this->container->has('security.token_storage')) {
throw new \LogicException('The SecurityBundle is not registered in your application.');
}
if (null === $token = $this->container->get('security.token_storage')->getToken()) {
return;
}
if (!is_object($user = $token->getUser())) {
// e.g. anonymous authentication
return;
}
// Redirect disabled users to a info page
if (!$user->isEnabled() && !$this instanceof InfoController) {
return $this->redirectToRoute('path_to_info');
}
return $user;
}
I'd like to be able to verify whether or not attributes (roles) are granted to any arbitrary object implementing UserInterface in Symfony2. Is this possible?
UserInterface->getRoles() is not suitable for my needs because it does not take the role hierarchy into account, and I'd rather not reinvent the wheel in that department, which is why I'd like to use the Access Decision Manager if possible.
Thanks.
In response to Olivier's solution below, here is my experience:
You can use the security.context service with the isGranted method. You can pass a second argument which is your object.
$user = new Core\Model\User();
var_dump($user->getRoles(), $this->get('security.context')->isGranted('ROLE_ADMIN', $user));
Output:
array (size=1)
0 => string 'ROLE_USER' (length=9)
boolean true
My role hierarchy:
role_hierarchy:
ROLE_USER: ~
ROLE_VERIFIED_USER: [ROLE_USER]
ROLE_ADMIN: [ROLE_VERIFIED_USER]
ROLE_SUPERADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
ROLE_ALLOWED_TO_SWITCH: ~
My UserInterface->getRoles() method:
public function getRoles()
{
$roles = [$this->isVerified() ? 'ROLE_VERIFIED_USER' : 'ROLE_USER'];
/**
* #var UserSecurityRole $userSecurityRole
*/
foreach ($this->getUserSecurityRoles() as $userSecurityRole) {
$roles[] = $userSecurityRole->getRole();
}
return $roles;
}
ROLE_ADMIN must be explicitly assigned, yet isGranted('ROLE_ADMIN', $user) returns TRUE even if the user was just created and has not been assigned any roles other than the default ROLE_USER, as long as the currently logged in user is granted ROLE_ADMIN. This leads me to believe the 2nd argument to isGranted() is just ignored and that the Token provided to AccessDecisionManager->decide() by the SecurityContext is used instead.
If this is a bug I'll submit a report, but maybe I'm still doing something wrong?
You need only AccessDecisionManager for this, no need for security context since you don't need authentication.
$user = new Core\Model\User();
$token = new UsernamePasswordToken($user, 'none', 'none', $user->getRoles());
$isGranted = $this->get('security.access.decision_manager')
->decide($token, array('ROLE_ADMIN'));
This will correctly take role hierarchy into account, since RoleHierarchyVoter is registered by default
Update
As noted by #redalaanait, security.access.decision_manager is a private service, so accessing it directly is not a good thing to do.
It's better to use service aliasing, which allows you to access private services.
security.context Is deprecated since 2.6.
Use AuthorizationChecker:
$token = new UsernamePasswordToken(
$user,
null,
'secured_area',
$user->getRoles()
);
$tokenStorage = $this->container->get('security.token_storage');
$tokenStorage->setToken($token);
$authorizationChecker = new AuthorizationChecker(
$tokenStorage,
$this->container->get('security.authentication.manager'),
$this->container->get('security.access.decision_manager')
);
if (!$authorizationChecker->isGranted('ROLE_ADMIN')) {
throw new AccessDeniedException();
}
Maybe you can instantiate a new securityContext instance and use it to check if user is granted :
$securityContext = new \Symfony\Component\Security\Core\SecurityContext($this->get('security.authentication.manager'), $this->get('security.access.decision_manager'));
$token = new \Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken($user, null, $this->container->getParameter('fos_user.firewall_name'), $user->getRoles());
$securityContext->setToken($token);
if ($securityContext->isGranted('ROLE_ADMIN')) {
// some stuff to do
}
I know this post is quite old, but I faced that problem recently and I created a service based on #dr.scre answer.
Here's how I did in Symfony 5.
<?php
declare(strict_types=1);
namespace App\Service;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
final class AccessDecisionMaker
{
private AccessDecisionManagerInterface $accessDecisionManager;
public function __construct(AccessDecisionManagerInterface $accessDecisionManager)
{
$this->accessDecisionManager = $accessDecisionManager;
}
public function isGranted(UserInterface $user, string $role): bool
{
$token = new UsernamePasswordToken($user, 'none', 'none', $user->getRoles());
return $this->accessDecisionManager->decide($token, [$role]);
}
}
Now I can use it wherever I want.
<?php
declare(strict_types=1);
namespace App\Service;
use App\Entity\User;
use Symfony\Component\Security\Core\Security;
class myClass
{
private Security $security;
private AccessDecisionMaker $decisionMaker;
public function __construct(Security $security, AccessDecisionMaker $decisionMaker)
{
$this->security = $security;
$this->decisionMaker = $decisionMaker;
}
public function someMethod(?User $user): void
{
$user = $user ?: $this->security->getUser();
if ($this->decisionMaker->isGranted($user, 'ROLE_SOME_ROLE')) {
// do something
} else {
// do something else
}
}
}
RoleVoter disregards the $object passed through from SecurityContext->isGranted(). This results in the RoleHierarchyVoter extracting roles from the Token instead of a provided UserInterface $object (if exists), so I had to find a different route.
Maybe there is a better way to go about this and if there is I'd sure like to know, but this is the solution I came up with:
First I implemented ContainerAwareInterface in my User class so I could access the security component from within it:
final class User implements AdvancedUserInterface, ContainerAwareInterface
{
// ...
/**
* #var ContainerInterface
*/
private $container;
// ...
public function setContainer(ContainerInterface $container = null)
{
if (null === $container) {
throw new \Exception('First argument to User->setContainer() must be an instance of ContainerInterface');
}
$this->container = $container;
}
// ...
}
Then I defined a hasRole() method:
/**
* #param string|\Symfony\Component\Security\Core\Role\RoleInterface $roleToCheck
* #return bool
* #throws \InvalidArgumentException
*/
public function hasRole($roleToCheck)
{
if (!is_string($roleToCheck)) {
if (!($roleToCheck instanceof \Symfony\Component\Security\Core\Role\RoleInterface)) {
throw new \InvalidArgumentException('First argument expects a string or instance of RoleInterface');
}
$roleToCheck = $roleToCheck->getRole();
}
/**
* #var \Symfony\Component\Security\Core\SecurityContext $thisSecurityContext
*/
$thisSecurityContext = $this->container->get('security.context');
$clientUser = $thisSecurityContext->getToken()->getUser();
// determine if we're checking a role on the currently authenticated client user
if ($this->equals($clientUser)) {
// we are, so use the AccessDecisionManager and voter system instead
return $thisSecurityContext->isGranted($roleToCheck);
}
/**
* #var \Symfony\Component\Security\Core\Role\RoleHierarchy $thisRoleHierarchy
*/
$thisRoleHierarchy = $this->container->get('security.role_hierarchy');
$grantedRoles = $thisRoleHierarchy->getReachableRoles($this->getRoles());
foreach ($grantedRoles as $grantedRole) {
if ($roleToCheck === $grantedRole->getRole()) {
return TRUE;
}
}
return FALSE;
}
From a controller:
$user = new User();
$user->setContainer($this->container);
var_dump($user->hasRole('ROLE_ADMIN'));
var_dump($this->get('security.context')->isGranted('ROLE_ADMIN'));
var_dump($this->get('security.context')->isGranted('ROLE_ADMIN', $user));
$user->addUserSecurityRole('ROLE_ADMIN');
var_dump($user->hasRole('ROLE_ADMIN'));
Output:
boolean false
boolean true
boolean true
boolean true
Although it does not involve the AccessDecisionManager or registered voters (unless the instance being tested is the currently authenticated user), it is sufficient for my needs as I just need to ascertain whether or not a given user has a particular role.
This looks like an issue with the:
abstract class AbstractToken implements TokenInterface
Look at the constructor. Looks like roles are created on instantiation and not queried at run time.
public function __construct(array $roles = array())
{
$this->authenticated = false;
$this->attributes = array();
$this->roles = array();
foreach ($roles as $role) {
if (is_string($role)) {
$role = new Role($role);
} elseif (!$role instanceof RoleInterface) {
throw new \InvalidArgumentException(sprintf('$roles must be an array of strings, or RoleInterface instances, but got %s.', gettype($role)));
}
$this->roles[] = $role;
}
}
Hence, the roles cannot change after the token has been created. I think the option is to write your own voter. I'm still looking around.
Create a service AccessDecisionMaker (used Shady's solution)
<?php
namespace Bp\CommonBundle\Service;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\Security\Core\Role\RoleInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\SecurityContext;
class AccessDecisionMaker
{
/** #var Container */
private $container;
/** #var SecurityContext */
private $securityContext;
function __construct($container)
{
$this->container = $container;
if (!$this->securityContext) {
// Ensure security context is created only once
$this->securityContext = new SecurityContext($this->container->get(
'security.authentication.manager'
), $this->container->get('security.access.decision_manager'));
}
}
public function isGranted($roleToCheck, UserInterface $user)
{
if (!is_string($roleToCheck)) {
if (!($roleToCheck instanceof RoleInterface)) {
throw new \InvalidArgumentException('First argument expects a string or instance of RoleInterface');
}
$roleToCheck = $roleToCheck->getRole();
}
$token = new UsernamePasswordToken($user, null, $this->container->getParameter(
'fos_user.firewall_name'
), $user->getRoles());
$this->securityContext->setToken($token);
if ($this->securityContext->isGranted($roleToCheck)) {
return true;
}
return false;
}
}
Configure this as a service
bp.access_decision_maker:
class: Bp\CommonBundle\Service\AccessDecisionMaker
arguments: [#service_container ]
Use it
$this->container->get('bp.access_decision_maker')->isGranted("ROLE_ADMIN",$user);
You can use the security.context service with the isGranted method.
You can pass a second argument which is your object (see here).
In a controller:
$this->get('security.context')->isGranted('ROLE_FOOBAR', $myUser)
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));
}
I would like to check from inside a controller if it is a secured page or not.
How to do this ?
My use case is the following :
User can register and log in
If he logs in and tries to access a secured page, he will be redirected to a "beta version" page until the end of June.
If he tries to access a normal page (not secured), he will be able to access it without any redirection.
Thanks for your help !
Aurel
When Symfony2 processes a request it matches the url pattern with each firewall defined in app/config/security.yml. When url pattern matches with a pattern of the firewall Symfony2 creates some listener objects and call handle method of those objects. If any listener returns a Response object then the loop breaks and Symfony2 outputs the response. Authentication part is done in authentication listeners. They are created from config defined in matched firewall e.g form_login, http_basic etc. If user is not authenticated then authenticated listeners create a RedirectResponse object to redirect user to login page. For your case, you can cheat by creating a custom authentication listener and add it in your secured page firewall. Sample implementation would be following,
Create a Token class,
namespace Your\Namespace;
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
class MyToken extends AbstractToken
{
public function __construct(array $roles = array())
{
parent::__construct($roles);
}
public function getCredentials()
{
return '';
}
}
Create a class that implements AuthenticationProviderInterface. For form_login listener it authenticates with the given UserProvider. In this case it will do nothing.
namespace Your\Namespace;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
use Acme\BaseBundle\Firewall\MyToken;
class MyAuthProvider implements AuthenticationProviderInterface
{
public function authenticate(TokenInterface $token)
{
if (!$this->supports($token)) {
return null;
}
throw new \Exception('you should not get here');
}
public function supports(TokenInterface $token)
{
return $token instanceof MyToken;
}
Create an entry point class. The listener will create a RedirectResponse from this class.
namespace Your\Namespace;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
use Symfony\Component\Security\Http\HttpUtils;
class MyAuthenticationEntryPoint implements AuthenticationEntryPointInterface
{
private $httpUtils;
private $redirectPath;
public function __construct(HttpUtils $httpUtils, $redirectPath)
{
$this->httpUtils = $httpUtils;
$this->redirectPath = $redirectPath;
}
/**
* {#inheritdoc}
*/
public function start(Request $request, AuthenticationException $authException = null)
{
//redirect action goes here
return $this->httpUtils->createRedirectResponse($request, $this->redirectPath);
}
Create a listener class. Here you will implement your redirection logic.
namespace Your\Namespace;
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
class MyAuthenticationListener implements ListenerInterface
{
private $securityContext;
private $authenticationEntryPoint;
public function __construct(SecurityContextInterface $securityContext, AuthenticationEntryPointInterface $authenticationEntryPoint)
{
$this->securityContext = $securityContext;
$this->authenticationEntryPoint = $authenticationEntryPoint;
}
public function handle(GetResponseEvent $event)
{
$token = $this->securityContext->getToken();
$request = $event->getRequest();
if($token === null){
return;
}
//add your logic
$redirect = // boolean value based on your logic
if($token->isAuthenticated() && $redirect){
$response = $this->authenticationEntryPoint->start($request);
$event->setResponse($response);
return;
}
}
}
Create the services.
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="my_firewall.security.authentication.listener"
class="Your\Namespace\MyAuthenticationListener"
parent="security.authentication.listener.abstract"
abstract="true">
<argument type="service" id="security.context" />
<argument /> <!-- Entry Point -->
</service>
<service id="my_firewall.entry_point" class="Your\Namespace\MyAuthenticationEntryPoint" public="false" ></service>
<service id="my_firewall.auth_provider" class="Your\Namespace\MyAuthProvider" public="false"></service>
</services>
</container>
Register the listener. Create a folder named Security/Factory in your bundles DependencyInjection folder. Then create the factory class.
namespace Your\Bundle\DependencyInjection\Security\Factory;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
class MyFirewallFactory implements SecurityFactoryInterface
{
public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
{
$provider = 'my_firewall.auth_provider.'.$id;
$container->setDefinition($provider, new DefinitionDecorator('my_firewall.auth_provider'));
// entry point
$entryPointId = $this->createEntryPoint($container, $id, $config, $defaultEntryPoint);
// listener
$listenerId = 'my_firewall.security.authentication.listener'.$id;
$listener = $container->setDefinition($listenerId, new DefinitionDecorator('my_firewall.security.authentication.listener'));
$listener->replaceArgument(1, new Reference($entryPointId));
return array($provider, $listenerId, $entryPointId);
}
public function getPosition()
{
return 'pre_auth';
}
public function getKey()
{
return 'my_firewall'; //the listener name
}
protected function getListenerId()
{
return 'my_firewall.security.authentication.listener';
}
public function addConfiguration(NodeDefinition $node)
{
$node
->children()
->scalarNode('redirect_path')->end()
->end()
;
}
protected function createEntryPoint($container, $id, $config, $defaultEntryPointId)
{
$entryPointId = 'my_firewall.entry_point'.$id;
$container
->setDefinition($entryPointId, new DefinitionDecorator('my_firewall.entry_point'))
->addArgument(new Reference('security.http_utils'))
->addArgument($config['redirect_path'])
;
return $entryPointId;
}
}
Then in your NamespaceBundle.php of your bundle folder add the following code.
public function build(ContainerBuilder $builder){
parent::build($builder);
$extension = $builder->getExtension('security');
$extension->addSecurityListenerFactory(new Security\Factory\MyFirewallFactory());
}
Authentication listener is created, phew :). Now in your app/config/security.yml do following.
api_area:
pattern: ^/secured/
provider: fos_userbundle
form_login:
check_path: /login_check
login_path: /login
csrf_provider: form.csrf_provider
my_firewall:
redirect_path: /beta
logout: true
anonymous: true
I dont know if this is the correct method
But you can try the following
/vendor/symfony/src/Symfony/Component/HttpKernel/HttpKernel.php has a method handleRaw that converts a request to a response . The request object is accessable from there . You can check if the client has requested to access the secured page . If so , you can manually set the controller like
$request->attributes->set('_controller','your\Bundle\SecureBundle\Controller\SecureController::secureAction');
Another solution would be to set a session if the user try to access a secured page and check for the same inside your controller
Again , this might not be the correct method but it is a possible workaround