Skip to content

Commit

Permalink
[4.x] Add ability to impersonate a user (#8622)
Browse files Browse the repository at this point in the history
* Add ability to impersonate a user

* Remove testing

* Use correct permission name

* Only impersonate one person at a time

* 🍺

* For @jacksleight, because he asked so nicely

* Tidy up and add config to enable/disable

* We don't actually need this

* Add a toast success message

* Tweak the Stop Impersonating overlay button

* Add Stop Impersonating to user avatar dropdown, give it an animation

* fix code style

* Why don't you fix it yourself mr lint?

---------

Co-authored-by: Jack McDade <[email protected]>
  • Loading branch information
ryanmitchell and jackmcdade authored Sep 19, 2023
1 parent f23ef9e commit 438ad5a
Show file tree
Hide file tree
Showing 13 changed files with 243 additions and 6 deletions.
1 change: 0 additions & 1 deletion config/cp.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,5 +125,4 @@
'custom_favicon_url' => env('STATAMIC_CUSTOM_FAVICON_URL', null),

'custom_css_url' => env('STATAMIC_CUSTOM_CSS_URL', null),

];
13 changes: 13 additions & 0 deletions config/users.php
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,17 @@
'web' => 'web',
],

/*
|--------------------------------------------------------------------------
| Impersonation
|--------------------------------------------------------------------------
|
| Here you can configure if impersonation is available, and what URL to
| redirect to after impersonation begins
|
*/
'impersonate' => [
'enabled' => env('STATAMIC_IMPERSONATE_ENABLED', true),
'redirect' => env('STATAMIC_IMPERSONATE_REDIRECT', null),
],
];
24 changes: 24 additions & 0 deletions resources/css/components/global-header.css
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,27 @@
max-height: 32px;
max-width: 280px;
}

@keyframes rotate {
100% {
transform: rotate(1turn);
}
}

.animate-radar {
@apply relative z-0;

&::after {
@apply absolute bg-transparent bg-no-repeat;
content: '';
z-index: 2;
left: -50%;
top: -50%;
width: 200%;
height: 200%;
background-size: 50% 50%, 50% 50%;
background-position: 0 0, 100% 0, 100% 100%, 0 100%;
background-image: linear-gradient(transparent, rgba(255,255,255,.7), transparent);
animation: rotate 4s linear infinite;
}
}
1 change: 1 addition & 0 deletions resources/lang/en/permissions.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
'edit_roles' => 'Edit roles',
'assign_user_groups' => 'Assign groups to users',
'assign_roles' => 'Assign roles to users',
'impersonate_users' => 'Impersonate users',

'group_updates' => 'Updates',
'view_updates' => 'View updates',
Expand Down
32 changes: 32 additions & 0 deletions resources/views/impersonator/terminate.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<style>
#__impersonator-link__ {
position: fixed;
display: flex;
z-index: 100;
align-items: center;
bottom: 0;
right: 0;
padding: 6px 10px;
color: #726e23;
font-size: 14px;
border-radius: 4px 0 0 0;
background: #fbfab0;
border-left: 1px solid #e8dc1e;
border-top: 1px solid #e8dc1e;
text-decoration: none;
transform: translateX(calc(100% - 35px));
transition: transform 0.21s cubic-bezier(0.11, 0, 0.5, 0);
box-shadow: -1px -2px 4px 0 rgba(20,20,20,.02);
}
#__impersonator-link__:hover {
transform: translateX(0);
}
#__impersonator-link__ svg {
margin-right: 10px;
}
</style>

<a id="__impersonator-link__" href="{{ $url }}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" height="20" width="20" viewBox="-0.5 -0.5 14 14"><g><path fill="#fbfab0" d="M0.4642857142857143 4.494285714285715C0.4643683571428572 5.727465714285715 0.8797935714285715 6.924672857142857 1.6435714285714287 7.892857142857143l2.35651 1.8977214285714286c0.3400057142857143 0.2738357142857143 0.8248314285714285 0.2738357142857143 1.164837142857143 0L7.521428571428571 7.892857142857143c0.7637778571428572 -0.9681842857142857 1.179202142857143 -2.1653914285714286 1.1792857142857143 -3.398571428571429V1.4671428571428573C7.427884285714286 0.8084895000000001 6.0156478571428575 0.4647388571428572 4.5825 0.4647388571428572 3.149352142857143 0.4647388571428572 1.7371157142857143 0.8084895000000001 0.4642857142857143 1.4671428571428573v3.027142857142857Z" stroke-width="1"></path><path id="Vector_2" stroke="#726e23" stroke-linecap="round" stroke-linejoin="round" d="M1.6435714285714287 7.892857142857143C0.8797935714285715 6.924672857142857 0.4643683571428572 5.727465714285715 0.4642857142857143 4.494285714285715V1.4671428571428573C1.7371157142857143 0.8084895000000001 3.149352142857143 0.4647388571428572 4.5825 0.4647388571428572c1.4331478571428573 0 2.8453842857142857 0.34375064285714285 4.118214285714285 1.002404" stroke-width="1"></path><path id="Vector_3" fill="#e8dc1e" d="M7.23359 12.48c-0.3249535714285714 0.07345 -0.6626842857142857 0.06880714285714286 -0.9855021428571429 -0.013464285714285715 -0.32282714285714287 -0.08236428571428572 -0.62153 -0.2400357142857143 -0.8716407142857143 -0.4601071428571429 -0.8689107142857143 -0.7490785714285715 -1.5123364285714287 -1.7250071428571427 -1.8583500000000002 -2.8188085714285713 -0.3460042857142857 -1.0937828571428572 -0.3809557142857143 -2.2622507142857144 -0.1009357142857143 -3.374762857142857l0.6778571428571428 -2.7114285714285713c1.4722407142857143 -0.37337857142857145 3.0141428571428572 -0.37540285714285715 4.487349285714286 -0.005887142857142857C10.055592857142857 3.465057142857143 11.414 4.194561428571428 12.535714285714286 5.218571428571429l-0.6778571428571428 2.7021428571428574c-0.2732785714285714 1.113515 -0.8502 2.129307142857143 -1.6666928571428572 2.9342857142857146 -0.8164 0.8049785714285714 -1.8402985714285713 1.3675071428571428 -2.957574285714286 1.625Z" stroke-width="1"></path><path id="Vector_4" stroke="#726e23" stroke-linecap="round" stroke-linejoin="round" d="M7.233534285714286 12.48c-0.3249535714285714 0.07345 -0.6626842857142857 0.06880714285714286 -0.9855021428571429 -0.013464285714285715 -0.32282714285714287 -0.08236428571428572 -0.62153 -0.2400357142857143 -0.8716407142857143 -0.4601071428571429v0c-0.8689107142857143 -0.7490785714285715 -1.5123364285714287 -1.7250071428571427 -1.8583500000000002 -2.8188085714285713 -0.3460042857142857 -1.0937828571428572 -0.3809557142857143 -2.2622507142857144 -0.1009357142857143 -3.374762857142857l0.6778571428571428 -2.7114285714285713c1.4722407142857143 -0.37337857142857145 3.0141428571428572 -0.37540285714285715 4.487349285714286 -0.005887142857142857C10.0555 3.465057142857143 11.413907142857143 4.194561428571428 12.535714285714286 5.218571428571429l-0.6778571428571428 2.7021428571428574c-0.2732785714285714 1.113515 -0.8502928571428572 2.129307142857143 -1.6666928571428572 2.9342857142857146 -0.8164928571428571 0.8049785714285714 -1.8403635714285715 1.3675071428571428 -2.95763 1.625v0Z" stroke-width="1"></path><path id="Vector_5" stroke="#726e23" stroke-linecap="round" stroke-linejoin="round" d="M5.209276428571428 6.045c0.11713000000000001 -0.12258071428571428 0.2655992857142857 -0.21075785714285716 0.42928785714285717 -0.25494857142857147 0.16369785714285714 -0.0442 0.3363657142857143 -0.04273285714285714 0.49928357142857144 0.004234285714285714 0.15945428571428574 0.037514285714285715 0.30615928571428574 0.11654500000000001 0.4252114285714286 0.22906928571428573 0.11905214285714286 0.11253357142857144 0.20620785714285714 0.2545492857142857 0.2526457142857143 0.411645" stroke-width="1"></path><path id="Vector_6" stroke="#726e23" stroke-linecap="round" stroke-linejoin="round" d="M8.886437857142857 6.964276428571429c0.11338785714285714 -0.12185642857142859 0.2574464285714286 -0.21099 0.417105 -0.25807785714285714 0.15962142857142858 -0.04708785714285714 0.32899285714285714 -0.050393571428571426 0.49037857142857144 -0.009573571428571428 0.1613857142857143 0.04081071428571429 0.30875 0.12425214285714287 0.42686428571428575 0.2415864285714286 0.11802142857142857 0.117325 0.20242857142857143 0.2642342857142857 0.24421428571428574 0.4253507142857143" stroke-width="1"></path></g></svg>
{{ __('Stop impersonating') }}
</a>
13 changes: 9 additions & 4 deletions resources/views/partials/global-header.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,24 +89,29 @@
</a>
<dropdown-list v-cloak>
<template v-slot:trigger>
<a class="dropdown-toggle items-center ml-4 h-full hide flex">
<a class="dropdown-toggle items-center ml-4 h-full hide flex relative group">
@if ($user->avatar())
<div class="icon-header-avatar"><img src="{{ $user->avatar() }}" /></div>
<div class="icon-header-avatar {{ session()->get('statamic_impersonated_by') ? 'animate-radar' : '' }}"><img src="{{ $user->avatar() }}" /></div>
@else
<div class="icon-header-avatar icon-user-initials">{{ $user->initials() }}</div>
<div class="icon-header-avatar {{ session()->get('statamic_impersonated_by') ? 'animate-radar' : '' }} icon-user-initials">{{ $user->initials() }}</div>
@endif
</a>
</template>

<div class="px-2">
<div class="text-base mb-px">{{ $user->email() }}</div>
@if ($user->isSuper())
<div class="text-2xs mt-px text-gray-600">{{ __('Super Admin') }}</div>
<div class="text-2xs mt-px text-gray-600">{{ __('Super Admin') }} @if (session()->get('statamic_impersonated_by'))(Impersonating)@endif</div>
@elseif (session()->get('statamic_impersonated_by'))
<div class="text-2xs mt-px text-gray-600">{{ __('Impersonating') }}</div>
@endif
</div>
<div class="divider"></div>

<dropdown-item :text="__('Profile')" redirect="{{ route('statamic.cp.account') }}"></dropdown-item>
@if (session()->get('statamic_impersonated_by'))
<dropdown-item :text="__('Stop Impersonating')" redirect="{{ cp_route('impersonation.stop') }}"></dropdown-item>
@endif
<dropdown-item :text="__('Log out')" redirect="{{ route('statamic.cp.logout', ['redirect' => cp_route('index')]) }}"></dropdown-item>
</dropdown-list>
</div>
Expand Down
3 changes: 3 additions & 0 deletions routes/cp.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use Statamic\Http\Controllers\CP\Auth\CsrfTokenController;
use Statamic\Http\Controllers\CP\Auth\ExtendSessionController;
use Statamic\Http\Controllers\CP\Auth\ForgotPasswordController;
use Statamic\Http\Controllers\CP\Auth\ImpersonationController;
use Statamic\Http\Controllers\CP\Auth\LoginController;
use Statamic\Http\Controllers\CP\Auth\ResetPasswordController;
use Statamic\Http\Controllers\CP\Auth\UnauthorizedController;
Expand Down Expand Up @@ -111,6 +112,8 @@
Route::get('extend', ExtendSessionController::class)->name('extend');

Route::get('unauthorized', UnauthorizedController::class)->name('unauthorized');

Route::get('stop-impersonating', [ImpersonationController::class, 'stop'])->name('impersonation.stop');
});

Route::middleware('statamic.cp.authenticated')->group(function () {
Expand Down
70 changes: 70 additions & 0 deletions src/Actions/Impersonate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

namespace Statamic\Actions;

use Illuminate\Events\NullDispatcher;
use Illuminate\Support\Facades\Auth;
use Statamic\Contracts\Auth\User as UserContract;
use Statamic\Facades\CP\Toast;
use Statamic\Facades\User;

class Impersonate extends Action
{
protected $confirm = false;

public static function title()
{
return __('Start Impersonating');
}

public function visibleTo($item)
{
if (! config('statamic.users.impersonate.enabled', true) || session()->get('statamic_impersonated_by')) {
return false;
}

return $item instanceof UserContract && $item != User::current();
}

public function visibleToBulk($items)
{
return false;
}

public function authorize($authed, $user)
{
return $authed->can('impersonate users');
}

public function run($users, $values)
{
$guard = Auth::guard();

$dispatcher = $guard->getDispatcher();

if ($dispatcher) {
$guard->setDispatcher(new NullDispatcher($dispatcher));
}

try {
$currentUser = $guard->user();

$guard->login($users->first());
session()->put('statamic_impersonated_by', $currentUser->getKey());
Toast::success(__('You are now impersonating').' '.$users->first()->name());
} finally {
if ($dispatcher) {
$guard->setDispatcher($dispatcher);
}
}
}

public function redirect($users, $values)
{
if ($url = config('statamic.users.impersonate.redirect')) {
return $url;
}

return $users->first()->can('access cp') ? cp_route('dashboard') : '/';
}
}
1 change: 1 addition & 0 deletions src/Auth/CorePermissions.php
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ protected function registerUsers()

$this->register('edit user groups');
$this->register('edit roles');
$this->register('impersonate users');
}

protected function registerForms()
Expand Down
35 changes: 35 additions & 0 deletions src/Http/Controllers/CP/Auth/ImpersonationController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace Statamic\Http\Controllers\CP\Auth;

use Illuminate\Events\NullDispatcher;
use Illuminate\Support\Facades\Auth;
use Statamic\Facades\User;

class ImpersonationController
{
public function stop()
{
if ($originalUserId = session()->pull('statamic_impersonated_by')) {
$guard = Auth::guard();

$dispatcher = $guard->getDispatcher();

if ($dispatcher) {
$guard->setDispatcher(new NullDispatcher($dispatcher));
}

$originalUser = User::find($originalUserId);

if ($originalUser) {
$guard->login($originalUser);
}

if ($dispatcher) {
$guard->setDispatcher($dispatcher);
}
}

return redirect()->route('statamic.cp.users.index');
}
}
52 changes: 52 additions & 0 deletions src/Http/Middleware/StopImpersonating.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

namespace Statamic\Http\Middleware;

use Closure;
use Illuminate\Http\Response;

class StopImpersonating
{
public function handle($request, Closure $next)
{
$response = $next($request);

if (! $this->shouldInjectLink($response)) {
return $response;
}

return $this->injectLink($response);
}

private function shouldInjectLink($response)
{
if (! session()->get('statamic_impersonated_by')) {
return false;
}

if (! $response instanceof Response) {
return false;
}

if (! $content = $response->content()) {
return false;
}

if (stripos($content, '<html') === false) {
return false;
}

return true;
}

private function injectLink($response)
{
$content = $response->content();

$link = view('statamic::impersonator.terminate', [
'url' => route('statamic.cp.impersonation.stop'),
])->render();

return $response->setContent(str_replace('</body>', $link.'</body>', $content));
}
}
3 changes: 2 additions & 1 deletion src/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ public function boot()
->pushMiddleware(\Statamic\Http\Middleware\PoweredByHeader::class)
->pushMiddleware(\Statamic\Http\Middleware\CheckComposerJsonScripts::class)
->pushMiddleware(\Statamic\Http\Middleware\CheckMultisite::class)
->pushMiddleware(\Statamic\Http\Middleware\DisableFloc::class);
->pushMiddleware(\Statamic\Http\Middleware\DisableFloc::class)
->pushMiddleware(\Statamic\Http\Middleware\StopImpersonating::class);

$this->loadViewsFrom("{$this->root}/resources/views", 'statamic');

Expand Down
1 change: 1 addition & 0 deletions src/Providers/ExtensionServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class ExtensionServiceProvider extends ServiceProvider
Actions\ReuploadAsset::class,
Actions\MoveAssetFolder::class,
Actions\RenameAssetFolder::class,
Actions\Impersonate::class,
];

protected $fieldtypes = [
Expand Down

0 comments on commit 438ad5a

Please sign in to comment.