Allow switching/impersonating only to particular users in Symfony2 - symfony

EXPLANATION
Allowing user to switch to any other user is easy in Symfony2. My question is, how to allow certain user to switch only to certain other users? I'm using FOSUserBundle as my users provider.
EXAMPLE
There is 5 users, UserA, UserB, UserC, UserD, UserE, but only 3 of them are linked together.
UserA can switch to UserB and UserC
UserB can switch to UserA and UserC
UserC can switch to UserA and UserB
Thanks for your help!

You could possibly implement this type of thing by overriding the default SwitchUserListener.
The parameter you would override is: security.authentication.switchuser_listener.class
For example, in parameters.yml:
parameters:
security.authentication.switchuser_listener.class: My\Project\Security\Listener\SwitchUserListener
Where, in your custom listener class, you'd implement Symfony\Component\Security\Http\Firewall\ListenerInterface and use the existing Symfony\Component\Security\Http\Firewall\SwitchUserListener class as a basis for your custom implementation.

If you want to impersonate Admin Users in to Regular user there are some examples :)
https://gist.github.com/1589120
http://symfony.com/doc/current/book/security.html#impersonating-a-user

Thanks to Veonik, a (maybe dirty) way to do that
Add a role with that syntax :
ROLE_ALLOWED_TO_SWITCH_{usertype}
For example add ROLE_A to userA :
ROLE_A:
- ROLE_ALLOWED_TO_SWITCH_USERTYPEB
- ROLE_ALLOWED_TO_SWITCH_USERTYPEC
Declare your own SwitchUserListener service in yml :
security.authentication.switchuser_listener:
class: %security.authentication.switchuser_listener.class%
public: false
abstract: true
tags:
- { name: monolog.logger, channel: security}
arguments:
- #security.context
- null
- #security.user_checker
- null
- #security.access.decision_manager
- #logger
- _switch_user
- ROLE_ALLOWED_TO_SWITCH
- #event_dispatcher
- #security.role_hierarchy
In SwitchUserListener::attemptSwitchUser method
private function attemptSwitchUser(Request $request)
{
....
CODE BEFORE
....
$user = $this->provider->loadUserByUsername($username);
$rtoSwith = $this->getRolesToSwitchTo($token->getRoles());
$accessGranted = false;
foreach ($rtoSwith as $suType) {
$instance = ucfirst(strtolower($suType));
if ($user instanceof $instance) {
$accessGranted = true;
break;
}
}
if (!$accessGranted) {
throw new AccessDeniedException('Access Denied, not allowed to switch to type : '.get_class($user));
}
....
CODE AFTER
....
return $token
}
protected function getRolesToSwitchTo($roles){
$rts = array();
foreach ($this->roleHierarchy->getReachableRoles($roles) as $role) {
$tmp = explode("ROLE_ALLOWED_TO_SWITCH_", $role->getRole());
if(isset($tmp[1])){
$rts[] = $tmp[1];
}
}
return $rts;
}
Tell me if you have another solution
++

Related

Symfony get original user after impersonate another

I'm using Symfony 3.4 and I'm working with the Impersonate user feature : https://symfony.com/doc/3.4/security/impersonating_user.html
I need when I impersonate an user to get the original user.. I don't know how can I do that.
During impersonation, the user is provided with a special role called ROLE_PREVIOUS_ADMIN, is there a way to change this role ?
For example if my original user is ROLE_ADMIN, the special role is ROLE_PREVIOUS_ADMIN, but if my original user is ROLE_SOMETHING, the custom role should be : ROLE_PREVIOUS_SOMETHING
I just need to have a way to get the original user or at least get his roles.
Thanks !
I found a solution :
public function isImpersonatorAdmin()
{
$impersonatorUser = false;
if ($this->security->isGranted('ROLE_PREVIOUS_ADMIN')) {
foreach ($this->security->getToken()->getRoles() as $role) {
if ($role instanceof SwitchUserRole) {
$impersonatorUser = $role->getSource()->getUser()->hasRole('ROLE_ADMIN');
break;
}
}
}
return $impersonatorUser;
}
This function return true if the impersonator is ROLE_ADMIN.

Modify symfony redirect path after re-login

I have viewed a lot of similar questions but haven't found the answer.
Well, when the session expired, user will be redirected to the login page.
Then, user insert login/password and symfony redirects him to the previous page, like /page.
I want redirect user to #/page, so I need add /# string to referer path. How can I do that?
I'm using FOSUserBundle, but looks like that is what the symfony do.
Any ideas?
I have figure out the solution. We need to extend the Symfony security component DefaultAuthenticationSuccessHandler, to be more specific - the determineTargetUrl method.
This piece of code is responsible for the url, after session ends
if (null !== $this->providerKey && $targetUrl = $request->getSession()->get('_security.'.$this->providerKey.'.target_path')) {
$request->getSession()->remove('_security.'.$this->providerKey.'.target_path');
return $targetUrl;
}
So, let's extend this class and modify the $targetUrl value.
Firstable, create the handler, I have added the AuthenticationHandler.php
in the Vendor/YourBundle/Handle directory
<?php
namespace Vendor\YourBundle\Handler;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationSuccessHandler;
use Symfony\Component\Security\Http\ParameterBagUtils;
class AuthenticationHandler extends DefaultAuthenticationSuccessHandler
{
protected function determineTargetUrl(Request $request)
{
if ($this->options['always_use_default_target_path']) {
return $this->options['default_target_path'];
}
if ($targetUrl = ParameterBagUtils::getRequestParameterValue($request, $this->options['target_path_parameter'])) {
return $targetUrl;
}
if (null !== $this->providerKey && $targetUrl = $request->getSession()->get('_security.'.$this->providerKey.'.target_path')) {
$request->getSession()->remove('_security.'.$this->providerKey.'.target_path');
$arr = explode('//', $targetUrl);
$arr[1] = explode('/', $arr[1]);
$arr[1][0] .= "/#";
$arr[1] = implode('/', $arr[1]);
$arr = implode('//', $arr);
return $arr;
}
if ($this->options['use_referer'] && ($targetUrl = $request->headers->get('Referer')) && $targetUrl !== $this->httpUtils->generateUri($request, $this->options['login_path'])) {
return $targetUrl;
}
return $this->options['default_target_path'];
}
}
Register the service:
#services.yml
services:
authentication_handler:
class: Vendor\YourBundle\Handler\AuthenticationHandler
arguments: ["#security.http_utils", {}]
tags:
- { name: 'monolog.logger', channel: 'security' }
Define the handler:
#security.yml
form_login:
success_handler: authentication_handler
Enjoy!

fosuserbundle ldap configuration for strange use case

I'm trying to create a fosuserbundle for a quite strange use case, which is mandatory requirement, so no space to diplomacy.
Use case is as follow:
users in a mongo db table populated by jms messages -no registration form
users log in by ldap
user record not created by ldap, after a successful login username is checked against mongodb document
Considering that ldap could successfully log in people that exhist in ldap but cannot access site (but login is still successful), what could be the best way to perform such authentication chain?
I was thinking about some possible options:
listen on interactive login event, but imho there's no way to modify an onSuccess event
create a custom AuthenticationListener to do another check inside onSuccess method
chain authentication using scheb two-factor bundle
any hint?
I've used Fr3DLdapBundle which can be incorporate with FOSUserBundle quite easily (I'm using the 2.0.x version, I have no idea if the previous ones will do the same or be as easy to set up).
In the LdapManager (by default) it creates a new user if one is not already on the database which is not what I wanted (and doesn't seem to be what you want) so I have added my own manager that checks for the presence of the user in the database and then deals with the accordingly.
use FR3D\LdapBundle\Ldap\LdapManager as BaseLdapManager;
.. Other use stuff ..
class LdapManager extends BaseLdapManager
{
protected $userRepository;
protected $usernameCanonicalizer;
public function __construct(
LdapDriverInterface $driver,
$userManager,
array $params,
ObjectRepository $userRepository,
CanonicalizerInterface $usernameCanonicalizer
) {
parent::__construct($driver, $userManager, $params);
$this->userRepository = $userRepository;
$this->usernameCanonicalizer = $usernameCanonicalizer;
}
/**
* {#inheritDoc}
*/
public function findUserBy(array $criteria)
{
$filter = $this->buildFilter($criteria);
$entries = $this->driver->search(
$this->params['baseDn'], $filter, $this->ldapAttributes
);
if ($entries['count'] > 1) {
throw new \Exception('This search can only return a single user');
}
if ($entries['count'] == 0) {
return false;
}
$uid = $entries[0]['uid'][0];
$usernameCanonical = $this->usernameCanonicalizer->canonicalize($uid);
$user = $this->userRepository->findOneBy(
array('usernameCanonical' => $usernameCanonical)
);
if (null === $user) {
throw new \Exception('Your account has yet to be set up. See Admin.');
}
return $user;
}

Why does Symfony2 ACL go by username instead of ID?

I just started using Symfony's ACL system and was wondering why UserSecurityIdentity uses the username instead of the id of a User object to determine it's identity?
$user = new User();
$user->setId(new \MongoId());
$user->setUsername("frodo");
$dm->persist($user);
$uid = UserSecurityIdentity::fromAccount($user); // uses "frodo"
Our system allows users to alter their username, so using something more permanent (like the ID) to determine a user's identity seems more appropriate to me. Why was the ACL system implemented to use the username and not the ID? Any security considerations here?
This issue has been discussed here:
https://github.com/symfony/symfony/issues/5787
And has been solved in this commit:
https://github.com/symfony/symfony/commit/8d39213f4cca19466f84a5656a199eee98602ab1
So, now, whenever a user alter it's username, you can update its security indentity. I use a listener to do this:
public function preUpdate(PreUpdateEventArgs $eventArgs)
{
/** Update user security identity in case nick is changed * */
if ($entity instanceof \Acme\UserBundle\Entity\User && $eventArgs->hasChangedField('username')) {
$aclProvider = $this->container->get('security.acl.provider');
$securityId = UserSecurityIdentity::fromAccount($entity);
$aclProvider->updateUserSecurityIdentity($securityId, $eventArgs->getOldValue('username'));
}
}
This is implementation of fromAccount() method from Symfony\Component\Security\Acl\Domain\UserSecurityIdentity class:
public static function fromAccount(UserInterface $user)
{
return new self($user->getUsername(), ClassUtils::getRealClass($user));
}
I think it is answer on your question.

how to get the session variable in the view in symfony2

Thanks for your valuable suggestions
i have created a login system where i want to store the id's of users in session variables
this is my controller for login system
use Symfony\Component\HttpFoundation\Session\Session;
class successController extends Controller
{
public function successAction(Request $request)
{
--some code for form--
$repository = $em->getRepository('RepairStoreBundle:users');
$query = $repository->auth($name,$password);
$error="sorry invalid username or password";
if($query== false)
{
return $this->render('RepairLoginBundle:login:login.html.php', array(
'form' => $form->createView(),'error'=>$error,));
}
else
{
$role=$query[0]['role'];
$id=$query[0]['id'];
if($role == 1)
{
$session = new Session();
$session->start();
$session->set('id',$id);
$result=$repository->display();
return $this->render('RepairLoginBundle:login:success.html.php',array('result'=>$result,));
}
else
{
$session = new Session();
$session->start();
$session->set('id',$id);
$res= $repository->edit($id);
return $this->render('RepairLoginBundle:login:user.html.php',array('res'=>$res));
}
}
}
}
when admin logins with role=1 it will render to success.html.php
in this view how can i get the session variable which i have set in the controller.
i have used $session->get('id');
it is giving me server error please help with this
Upfront Authentication should better be done with the Security Component in Symfony2.
Read more about it in The Book - Security. You should probably also take a look at FOSUserBundle
Accessing the session from a PHP template in symfony2:
echo $app->getSession()->get('whatever');
Session Handling
There is an article in the official documentation:
Components/HttpFoundation - Session Data Management
The API documentation for the Session Component can be found here:
http://api.symfony.com/master/Symfony/Component/HttpFoundation/Session/Session.html
In the symfony2 standard-edition you can get the session from within a controller with:
$session = $this->getRequest()->getSession();
As you already have the request as an argument in successAction you could access the session with:
$session = $request->getSession();
Set a value with ( $value needs to be serializable ):
$session->set('key',$value);
Get a value with:
$session->get('key');
Saving (and closing) the session can be done with:
$session->save();
You should also loook at the SessionBag class.
you create a SessionBag and register it with the session. see:
Symfony API
In the registered SessionBag - which implements AttributeBagInterface - you can get and set your key/value's as desired.
TIP: If you want to get the current User and you have a container aware controller ( container injected )
you can use:
$user = $this->container->get('security.context')->getToken()->getUser();
if you are extending Symfony's Controller class in the standard-edition - the shorter way is:
$user = $this->get('security.context')->getToken()->getUser();
or even shorter (Symfony > 2.1.x):
$user = $this->getUser();
Alternative ( If your controller is not container aware ):
Define the controller as a service and inject #security.context:
YAML:
# src/Vendor/YourBundle/Resources/config/services.yml
services:
my.controller.service:
class: Vendor\YourBundle\Controller\successController
arguments: ["#security.context"]
Vendor\YourBundle\Controller\successController:
protected $securityContext;
public function __construct(SecurityContextInterface $securityContext)
{
$this->securityContext = $securityContext;
}
then in your action:
$user = $this->securityContext->getToken()->getUser();
Note:: you have to use the service in your routing aswell if you choose the controller-as-service variant. example routing.yml :
[...]
route_name:
pattern: /success
defaults: { _controller: my.controller.service:successAction }
[...]
[...]
Note... you can also inject the session with "#session"
# src/Vendor/YourBundle/Resources/config/services.yml
[...]
arguments: ["#security.context","#session"]
Note injecting the whole container is resource-heavy. advanced developers inject their needed services one-by-one and not the whole container.
Tip: Normally Controller classes are written with a capital first letter - example: *S*uccessController
General TIP: You have unnecessary dublicate code in your example:
// 'if' and 'else' execute the same stuff here
// result: dublicate code = more code = harder to read
if($role == 1)
{
$session = new Session();
$session->start();
[...]
}
else
{
$session = new Session();
$session->start();
[...]
}
should better be ...
// better: put this stuff before the if/else statement
$session = new Session();
$session->start();
if($role == 1)
{
[...]
}
else
{
[...]
}

Resources