From 438ad5a67693c457bf72fb4b3876b7084f5ddd27 Mon Sep 17 00:00:00 2001 From: Ryan Mitchell Date: Tue, 19 Sep 2023 06:13:57 +0100 Subject: [PATCH] [4.x] Add ability to impersonate a user (#8622) * Add ability to impersonate a user * Remove testing * Use correct permission name * Only impersonate one person at a time * :beer: * 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 --- config/cp.php | 1 - config/users.php | 13 ++++ resources/css/components/global-header.css | 24 +++++++ resources/lang/en/permissions.php | 1 + .../views/impersonator/terminate.blade.php | 32 +++++++++ .../views/partials/global-header.blade.php | 13 ++-- routes/cp.php | 3 + src/Actions/Impersonate.php | 70 +++++++++++++++++++ src/Auth/CorePermissions.php | 1 + .../CP/Auth/ImpersonationController.php | 35 ++++++++++ src/Http/Middleware/StopImpersonating.php | 52 ++++++++++++++ src/Providers/AppServiceProvider.php | 3 +- src/Providers/ExtensionServiceProvider.php | 1 + 13 files changed, 243 insertions(+), 6 deletions(-) create mode 100644 resources/views/impersonator/terminate.blade.php create mode 100644 src/Actions/Impersonate.php create mode 100644 src/Http/Controllers/CP/Auth/ImpersonationController.php create mode 100644 src/Http/Middleware/StopImpersonating.php diff --git a/config/cp.php b/config/cp.php index 188b7d9f5d..5a7566af49 100644 --- a/config/cp.php +++ b/config/cp.php @@ -125,5 +125,4 @@ 'custom_favicon_url' => env('STATAMIC_CUSTOM_FAVICON_URL', null), 'custom_css_url' => env('STATAMIC_CUSTOM_CSS_URL', null), - ]; diff --git a/config/users.php b/config/users.php index 190108586e..f51cf04a5d 100644 --- a/config/users.php +++ b/config/users.php @@ -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), + ], ]; diff --git a/resources/css/components/global-header.css b/resources/css/components/global-header.css index af874e040c..4c9f4e022b 100644 --- a/resources/css/components/global-header.css +++ b/resources/css/components/global-header.css @@ -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; + } +} diff --git a/resources/lang/en/permissions.php b/resources/lang/en/permissions.php index 4bc4a37807..d3186c9a8c 100644 --- a/resources/lang/en/permissions.php +++ b/resources/lang/en/permissions.php @@ -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', diff --git a/resources/views/impersonator/terminate.blade.php b/resources/views/impersonator/terminate.blade.php new file mode 100644 index 0000000000..ee771d4021 --- /dev/null +++ b/resources/views/impersonator/terminate.blade.php @@ -0,0 +1,32 @@ + + + + + {{ __('Stop impersonating') }} + diff --git a/resources/views/partials/global-header.blade.php b/resources/views/partials/global-header.blade.php index efc3edfa75..a4b288be5e 100644 --- a/resources/views/partials/global-header.blade.php +++ b/resources/views/partials/global-header.blade.php @@ -89,11 +89,11 @@ @@ -101,12 +101,17 @@
{{ $user->email() }}
@if ($user->isSuper()) -
{{ __('Super Admin') }}
+
{{ __('Super Admin') }} @if (session()->get('statamic_impersonated_by'))(Impersonating)@endif
+ @elseif (session()->get('statamic_impersonated_by')) +
{{ __('Impersonating') }}
@endif
+ @if (session()->get('statamic_impersonated_by')) + + @endif
diff --git a/routes/cp.php b/routes/cp.php index a4126bf6cc..b770215574 100644 --- a/routes/cp.php +++ b/routes/cp.php @@ -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; @@ -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 () { diff --git a/src/Actions/Impersonate.php b/src/Actions/Impersonate.php new file mode 100644 index 0000000000..c27d11c127 --- /dev/null +++ b/src/Actions/Impersonate.php @@ -0,0 +1,70 @@ +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') : '/'; + } +} diff --git a/src/Auth/CorePermissions.php b/src/Auth/CorePermissions.php index 1f480b26d7..a4008dbd54 100644 --- a/src/Auth/CorePermissions.php +++ b/src/Auth/CorePermissions.php @@ -174,6 +174,7 @@ protected function registerUsers() $this->register('edit user groups'); $this->register('edit roles'); + $this->register('impersonate users'); } protected function registerForms() diff --git a/src/Http/Controllers/CP/Auth/ImpersonationController.php b/src/Http/Controllers/CP/Auth/ImpersonationController.php new file mode 100644 index 0000000000..a5edc96afa --- /dev/null +++ b/src/Http/Controllers/CP/Auth/ImpersonationController.php @@ -0,0 +1,35 @@ +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'); + } +} diff --git a/src/Http/Middleware/StopImpersonating.php b/src/Http/Middleware/StopImpersonating.php new file mode 100644 index 0000000000..a30f221d2c --- /dev/null +++ b/src/Http/Middleware/StopImpersonating.php @@ -0,0 +1,52 @@ +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, 'content(); + + $link = view('statamic::impersonator.terminate', [ + 'url' => route('statamic.cp.impersonation.stop'), + ])->render(); + + return $response->setContent(str_replace('', $link.'', $content)); + } +} diff --git a/src/Providers/AppServiceProvider.php b/src/Providers/AppServiceProvider.php index 2ce3adcc8a..e356998ccc 100644 --- a/src/Providers/AppServiceProvider.php +++ b/src/Providers/AppServiceProvider.php @@ -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'); diff --git a/src/Providers/ExtensionServiceProvider.php b/src/Providers/ExtensionServiceProvider.php index 6d9705dcea..770dc12e93 100644 --- a/src/Providers/ExtensionServiceProvider.php +++ b/src/Providers/ExtensionServiceProvider.php @@ -45,6 +45,7 @@ class ExtensionServiceProvider extends ServiceProvider Actions\ReuploadAsset::class, Actions\MoveAssetFolder::class, Actions\RenameAssetFolder::class, + Actions\Impersonate::class, ]; protected $fieldtypes = [