diff --git a/src/Illuminate/Auth/SessionGuard.php b/src/Illuminate/Auth/SessionGuard.php index ebcf0de61fb0..f68923872911 100644 --- a/src/Illuminate/Auth/SessionGuard.php +++ b/src/Illuminate/Auth/SessionGuard.php @@ -210,6 +210,75 @@ protected function userFromRecaller($recaller) return $user; } + /** + * Impersonate the given user. + * + * @param \Illuminate\Contracts\Auth\Authenticatable $user + * @return void + * + * @throws \Illuminate\Auth\AuthenticationException + */ + public function impersonate(AuthenticatableContract $user) + { + if ($this->impersonating()) { + throw new AuthenticationException('Cannot impersonate while already impersonating.'); + } + + if (! $authenticated = $this->user()) { + throw new AuthenticationException('Cannot impersonate without a currently authenticated user.'); + } + + $this->session->put($this->getImpersonationName(), $authenticated->getAuthIdentifier()); + + $this->session->regenerate(); + + $this->login($user); + } + + /** + * Stop impersonating a user and resume the original authentication state. + * + * @return void + */ + public function unpersonate() + { + if (! $id = $this->session->pull($this->getImpersonationName())) { + return; + } + + if ($user = $this->provider->retrieveById($id)) { + $this->login($user); + + $this->session->regenerate(); + + return; + } + + $this->logout(); + } + + /** + * Get the underlying impersonator user. + * + * @return \Illuminate\Contracts\Auth\Authenticatable|null + */ + public function impersonator() + { + if ($id = $this->session->get($this->getImpersonationName())) { + return $this->provider->retrieveById($id); + } + } + + /** + * Determine if the current user is impersonating another user. + * + * @return bool + */ + public function impersonating() + { + return $this->session->has($this->getImpersonationName()); + } + /** * Get the decrypted recaller cookie for the request. * @@ -839,6 +908,16 @@ public function getRecallerName() return 'remember_'.$this->name.'_'.sha1(static::class); } + /** + * Get a unique identifier for the impersonator session value. + * + * @return string + */ + public function getImpersonationName() + { + return 'impersonator_'.$this->name.'_'.sha1(static::class); + } + /** * Determine if the user was authenticated via "remember me" cookie. * diff --git a/tests/Auth/AuthGuardTest.php b/tests/Auth/AuthGuardTest.php index d7df6decea9d..36b61d49e56b 100755 --- a/tests/Auth/AuthGuardTest.php +++ b/tests/Auth/AuthGuardTest.php @@ -651,6 +651,111 @@ public function testForgetUserSetsUserToNull() $this->assertNull($guard->getUser()); } + public function testImpersonate() + { + $mock = $this->getGuard(); + + $impersonator = m::mock(Authenticatable::class); + $impersonator->shouldReceive('getAuthIdentifier')->once()->andReturn('foo'); + + $impersonated = m::mock(Authenticatable::class); + $impersonated->shouldReceive('getAuthIdentifier')->once()->andReturn('bar'); + + $mock->getSession()->shouldReceive('has')->with($mock->getImpersonationName())->once()->andReturn(false); + $mock->getSession()->shouldReceive('get')->with($mock->getName())->once()->andReturn('foo'); + $mock->getProvider()->shouldReceive('retrieveById')->once()->with('foo')->andReturn($impersonator); + $mock->getSession()->shouldReceive('put')->with($mock->getImpersonationName(), 'foo')->once(); + $mock->getSession()->shouldReceive('put')->with($mock->getName(), 'bar')->once(); + $mock->getSession()->shouldReceive('regenerate')->once(); + $mock->getSession()->shouldReceive('migrate')->once(); + + $mock->impersonate($impersonated); + } + + public function testImpersonateThrowsWhenAlreadyImpersonating() + { + $this->expectException(AuthenticationException::class); + $this->expectExceptionMessage('Cannot impersonate while already impersonating.'); + + $mock = $this->getGuard(); + + $mock->getSession()->shouldReceive('has')->with($mock->getImpersonationName())->once()->andReturn(true); + + $mock->impersonate(m::mock(Authenticatable::class)); + } + + public function testImpersonateThrowsWhenUserIsNotAuthenticated() + { + $this->expectException(AuthenticationException::class); + $this->expectExceptionMessage('Cannot impersonate without a currently authenticated user.'); + + $mock = $this->getGuard(); + + $mock->getSession()->shouldReceive('has')->with($mock->getImpersonationName())->once()->andReturn(false); + $mock->getSession()->shouldReceive('get')->with($mock->getName())->once()->andReturn(null); + + $mock->impersonate(m::mock(Authenticatable::class)); + } + + public function testUnpersonate() + { + $mock = $this->getGuard(); + + $impersonator = m::mock(Authenticatable::class); + $impersonator->shouldReceive('getAuthIdentifier')->once()->andReturn('foo'); + + $mock->getSession()->shouldReceive('pull')->with($mock->getImpersonationName())->once()->andReturn('foo'); + $mock->getProvider()->shouldReceive('retrieveById')->once()->with('foo')->andReturn($impersonator); + $mock->getSession()->shouldReceive('put')->with($mock->getName(), 'foo')->once(); + $mock->getSession()->shouldReceive('regenerate')->once(); + $mock->getSession()->shouldReceive('migrate')->once(); + + $mock->unpersonate(); + } + + public function testUnpersonateLogsOutWhenImpersonatorIsNotAuthenticated() + { + [$session, $provider, $request] = $this->getMocks(); + + $mock = $this->getMockBuilder(SessionGuard::class)->onlyMethods(['clearUserDataFromStorage'])->setConstructorArgs(['default', $provider, $session, $request])->getMock(); + $mock->expects($this->once())->method('clearUserDataFromStorage'); + + $mock->setDispatcher($events = m::mock(Dispatcher::class)); + $events->shouldReceive('dispatch')->once()->with(m::type(Authenticated::class)); + + $session->shouldReceive('pull')->with($mock->getImpersonationName())->once()->andReturn('foo'); + $provider->shouldReceive('retrieveById')->once()->with('foo')->andReturn(null); + + $user = m::mock(Authenticatable::class); + $user->shouldReceive('getRememberToken')->andReturn(null); + $mock->setUser($user); + + $events->shouldReceive('dispatch')->once()->with(m::type(Logout::class)); + + $mock->unpersonate(); + } + + public function testImpersonator() + { + $mock = $this->getGuard(); + + $impersonator = m::mock(Authenticatable::class); + + $mock->getSession()->shouldReceive('get')->with($mock->getImpersonationName())->once()->andReturn('foo'); + $mock->getProvider()->shouldReceive('retrieveById')->once()->with('foo')->andReturn($impersonator); + + $this->assertSame($impersonator, $mock->impersonator()); + } + + public function testImpersonating() + { + $mock = $this->getGuard(); + + $mock->getSession()->shouldReceive('has')->with($mock->getImpersonationName())->once()->andReturnTrue(); + + $this->assertTrue($mock->impersonating()); + } + protected function getGuard() { [$session, $provider, $request, $cookie, $timebox] = $this->getMocks();