Switch Between Active Users

In my web application, I need the admin to have the ability to log in to the application and see exactly what other users see when they log in. The admins can view a lot of extra information that normal users cannot and it can cloud up the screen. I need the admin to see exactly what a specific user would see if he were logged in.

For example, if a user calls in and complains about a bug in the web application to an admin, that admin should have the ability to input that user’s ID and his views will change to look exactly the same as if he were the user calling in. The admin can then repeat the exact same steps that lead to an error. Additionally, the admin needs to have the ability to switch back to his normal role (preferably without having to log out and re-authenticate).

I have been trying to figure out how I would do this in Yii and have not come up with a good solution yet. The Yii::User is a read-only property.

Has anyone else tried this before, or have any ideas on how to implement this?

CWebUser (component that is visible as Yii::app()->user) has method ‘login’: http://www.yiiframework.com/doc/api/1.1/CWebUser#login-detail

to switch users you must simply pass another IUseIdentity object which basicly can look like this:




class SwitchIdentity extends CBaseUserIdentity {

  private $_id;

  private $_name;


  public function SwitchIdentity( $userId, $userName ) {

    $this->_id = $userId;

    $this->_name = $userName;

  }


  public function getId() {

    return $this->_id;

  }


  public function getName() {

    return $this->_name;

  }

}



so to switch user you have to do this:




$newIdentity = new SwitchIdentity( 'demo', 'demo' );

Yii::app()->user->login( $newIdentity );



to implement ‘switch back’ you have to somehow pass information that currently logged user was switched from admin. you can do this with user state:




$newIdentity = new SwitchIdentity( 'demo', 'demo' );

$newIdentity->setState( 'fromAdmin', true );

Yii::app()->user->login( $newIdentity );



and check it like this:




if( Yii::app()->user->hasState( 'fromAdmin' ) && Yii::app()->user->getState( 'fromAdmin' ) ) {

  ...

   //allow switch back action

}



This might seem like a smart alec answer but I’d say the best way to do this is to implement a logout action. Then your admin can logout and use the login form to test any user he wishes.

“krove”: you can’t use the login form when you don’t know user passwords.

Yes, this is basically correct but that isn’t exactly what I meant to say.

I don’t know what sort of user management system you’re using but the one I’m using lets me define user groups. I can be fairly certain that any user I create which is in the same group as another user will have the same permissions (I don’t like setting ‘per-user’ permissions because it makes this stuff more difficult to test). As long as I only use the group permission system to decide what is visible I’m ensured that what I see as one member of the group is going to be the same as what another member sees. This allows me to create accounts for developers to use for this purpose. Of course, the real world isn’t so perfect and often times other factors determine what is visible. Usually, those situations are easy to work around but depending on exactly why others are seeing things differently it may not be so easy to test for. The basic idea is to create accounts for each scenario you’ll be testing; if the scenario includes details which do not apply to group permissions then you’ll likely want to also include testing accounts which also include those differences.

I know that this isn’t quite as robust (or as easy to use) of a solution as what RedGuy posted. It is (I believe) a much more common and straight forward way to handle this problem and requires no additional code if you are already using a user group management system (which means there is nothing to break later). If implemented correctly, I don’t see any reason to that RedGuy’s solution is much less secure though so go ahead and take your pick.

First I get

[i]Class SwitchIdentity contains 1 abstract method and must therefore be declared abstract or implement the remaining methods

[/i]

After I put abstarct before it I tried:

[i]$newIdentity = new SwitchIdentity( ‘demo’, ‘demo’ );

[/i]

but I get

Class ‘SwitchIdentity’ not found

rather implement the method I missed. To create objects the class cannot be abstract…

looking at code the only missing method is:




public function authenticate();



which should be implemented just like this:




public function authenticate() {

   $this->errorCode = self::ERROR_NONE;

   return true;

}



yes, this is working now.

I also added the current id/username to state to know where to switch back (any better solution) as the site can have more than one admin.

I’ve also make the “switch back” link look like the “logout” one and displayed it instead when the impersonation happen.

Thanks

Dear Friends

So the concept is clearly laid out by redguy.

This is one implementation with ugly workarounds to exactly acheive what originally jhonka thought of.

So I am storing userIdentity across the requests and across the sessions as global state in runtime directory.

LoginForm.php




public function login()

	{

		if($this->_identity===null)

		{

			$this->_identity=new UserIdentity($this->username,$this->password);		

			$this->_identity->authenticate();

		}

		if($this->_identity->errorCode===UserIdentity::ERROR_NONE)

		{

			$duration=$this->rememberMe ? 3600*24*30 : 0; // 30 days

			$app=Yii::app();

			$app->user->login($this->_identity,$duration);


			$app->setGlobalState($app->user->id,$this->_identity);

			if($app->user->id==1) //admin

                           {

				$cookie=new CHttpCookie('user','admin');

				$app->request->getCookies()->add('user',$cookie);

			   }

			return true;

		}

		else

			return false;

	}

}




SiteController.php




public function actionListIdentity()

	{

		$dataProvider=new CActiveDataProvider('User');

		$this->render('listIdentity',array(

			'dataProvider'=>$dataProvider,

		));

	}



listIdentity.php




<?php $this->widget('zii.widgets.CListView', array(

	'dataProvider'=>$dataProvider,

	'itemView'=>'_view',

	'sortableAttributes'=>array('id')

)); ?>



_view.php




<div class="view">


	<b><?php echo CHtml::encode($data->getAttributeLabel('id')); ?>:</b>

	<?php echo CHtml::link(CHtml::encode($data->id), array('view', 'name'=>$data->username,'id'=>$data->id)); ?>

	<br />


	<b><?php echo CHtml::encode($data->getAttributeLabel('username')); ?>:</b>

	<?php echo CHtml::encode($data->username); ?>

	<br />	


	<b><?php echo CHtml::encode($data->getAttributeLabel('email')); ?>:</b>

	<?php echo CHtml::encode($data->email); ?>

	<br />


	

        <?php echo CHtml::link('Identify', array('identify','id'=>$data->id)); ?>


</div>



Again SiteController.php




public function actionIdentify($id)

{                                //admin identifying himself with other user.

		

     if(Yii::app()->user->id==1 ||(isset($_COOKIE['user'])&&$_COOKIE['user']=='admin'))

     {

	   $xUser=Yii::app()->getGlobalState($id);

           Yii::app()->user->login($xUser);

           $this->redirect(array('site/listIdentity'));

     }

		

     else throw new CHttpException('You are not authorised to do this action');

}



Both actions can be put in accessControllerFilter.

The side effect of putting actionIdentify in accessControllerFilter is user identity is dynamically changing.

So when admin assumes another user identity , it is difficult for him to revert back to his original identity

using the same action.

I think things can be acheived filters like ip filters..

The method CWebUser::changeIdentity is regenerating a new session deleting anything in the old session

when user identity is changed.

So I could not fetch the values stored in previous session to a newer sessions.

This forced me to store the admin identity in a cookie.

I think the better option may be to use IP FILTERS or similar sort of things in accessControllFilter.

Things are tested in my localhost with three different browsers not in remote servers.

Regards.

Why not use CWebUser’s changeIdentity? Keep a session variable indicating masked user is active and use changeIdentity again to switch back.




// Switch

$webUser = new BaseWebUser();

$webUser->changeIdentity($newUser->id, $newUser->username, array(

    'isMaskedUser' => true

));


// Switch back

$webUser = new BaseWebUser();

$webUser->changeIdentity($originalUser->id, $originalUser->username, array(

    'isMaskedUser' => false

));



Matt

well it is pretty same to what CWebUser::login( new ChangeIdentity(…) ) does :)

login() calls internally changeIdentity but also triggers onBeforeLogin, onAfterLogin… so it is up to you which one you want to use.