diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 00bf8cbe1..81bcfead9 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -36,6 +36,7 @@ class Kernel extends HttpKernel \BookStack\Http\Middleware\CheckEmailConfirmed::class, \BookStack\Http\Middleware\RunThemeActions::class, \BookStack\Http\Middleware\Localization::class, + \BookStack\Http\Middleware\Impersonate::class, ], 'api' => [ \BookStack\Http\Middleware\ThrottleApiRequests::class, diff --git a/app/Http/Middleware/Impersonate.php b/app/Http/Middleware/Impersonate.php new file mode 100644 index 000000000..b3c540a95 --- /dev/null +++ b/app/Http/Middleware/Impersonate.php @@ -0,0 +1,32 @@ +user(); + if ($realUser && $realUser->can(Permission::UsersManage)) { + Auth::onceUsingId($impersonateId); + } + + return $next($request); + } +} diff --git a/app/Users/Controllers/UserController.php b/app/Users/Controllers/UserController.php index 494221b14..4ab838fc6 100644 --- a/app/Users/Controllers/UserController.php +++ b/app/Users/Controllers/UserController.php @@ -191,6 +191,36 @@ class UserController extends Controller return view('users.delete', ['user' => $user]); } + /** + * Start impersonating the specified user. + */ + public function impersonate(int $id) + { + $this->checkPermission(Permission::UsersManage); + + $user = $this->userRepo->getById($id); + + if ($user->isGuest() || $user->id === user()->id) { + $this->showErrorNotification(trans('errors.users_cannot_impersonate')); + return redirect("/settings/users/{$id}"); + } + + session(['impersonate' => $user->id]); + + return redirect('/'); + } + + /** + * Stop impersonating and return to user edit page. + */ + public function stopImpersonate() + { + $userId = session('impersonate'); + session()->forget('impersonate'); + + return redirect("/settings/users/{$userId}"); + } + /** * Remove the specified user from storage. * diff --git a/lang/en/errors.php b/lang/en/errors.php index 20537d59f..9ee6ba0cf 100644 --- a/lang/en/errors.php +++ b/lang/en/errors.php @@ -78,6 +78,7 @@ return [ // Users 'users_cannot_delete_only_admin' => 'You cannot delete the only admin', 'users_cannot_delete_guest' => 'You cannot delete the guest user', + 'users_cannot_impersonate' => 'You cannot impersonate this user', 'users_could_not_send_invite' => 'Could not create user since invite email failed to send', // Roles diff --git a/lang/en/settings.php b/lang/en/settings.php index c4d1eb136..8448be5ae 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -231,6 +231,11 @@ return [ 'users_external_auth_id_desc' => 'When an external authentication system is in use (such as SAML2, OIDC or LDAP) this is the ID which links this BookStack user to the authentication system account. You can ignore this field if using the default email-based authentication.', 'users_password_warning' => 'Only fill the below if you would like to change the password for this user.', 'users_system_public' => 'This user represents any guest users that visit your instance. It cannot be used to log in but is assigned automatically.', + 'users_impersonate' => 'Impersonate User', + 'users_impersonate_desc' => 'Log in and browse the application as this user.', + 'users_impersonate_action' => 'Impersonate', + 'users_impersonating' => 'Impersonating: :name', + 'users_impersonate_stop' => 'Stop Impersonating', 'users_delete' => 'Delete User', 'users_delete_named' => 'Delete user :userName', 'users_delete_warning' => 'This will fully delete this user with the name \':userName\' from the system.', diff --git a/resources/views/layouts/base.blade.php b/resources/views/layouts/base.blade.php index b868cb888..78b8a11bc 100644 --- a/resources/views/layouts/base.blade.php +++ b/resources/views/layouts/base.blade.php @@ -54,6 +54,13 @@ @include('layouts.parts.base-body-start') @include('layouts.parts.skip-to-content') @include('layouts.parts.notifications') + @if(session('impersonate')) +
+ {{ trans('settings.users_impersonating', ['name' => user()->name]) }} +  |  + {{ trans('settings.users_impersonate_stop') }} +
+ @endif @include('layouts.parts.header')
diff --git a/resources/views/layouts/parts/header-user-menu.blade.php b/resources/views/layouts/parts/header-user-menu.blade.php index c252deb82..477760fb7 100644 --- a/resources/views/layouts/parts/header-user-menu.blade.php +++ b/resources/views/layouts/parts/header-user-menu.blade.php @@ -39,20 +39,27 @@

  • - @php - $logoutPath = match (config('auth.method')) { - 'saml2' => '/saml2/logout', - 'oidc' => '/oidc/logout', - default => '/logout', - } - @endphp -
    - {{ csrf_field() }} - - +
    {{ trans('settings.users_impersonate_stop') }}
    +
    + @else + @php + $logoutPath = match (config('auth.method')) { + 'saml2' => '/saml2/logout', + 'oidc' => '/oidc/logout', + default => '/logout', + } + @endphp +
    + {{ csrf_field() }} + +
    + @endif
  • \ No newline at end of file diff --git a/resources/views/users/edit.blade.php b/resources/views/users/edit.blade.php index 611653d6a..e0e9141fa 100644 --- a/resources/views/users/edit.blade.php +++ b/resources/views/users/edit.blade.php @@ -103,6 +103,17 @@ @endif @include('users.api-tokens.parts.list', ['user' => $user, 'context' => 'settings']) + + @if(!$user->isGuest() && $user->id !== user()->id && userCan('users-manage') && !session('impersonate')) +
    +

    {{ trans('settings.users_impersonate') }}

    +

    {{ trans('settings.users_impersonate_desc') }}

    +
    id}/impersonate") }}" method="post"> + {!! csrf_field() !!} + +
    +
    + @endif @stop diff --git a/routes/web.php b/routes/web.php index a20c0a3d3..248f9e380 100644 --- a/routes/web.php +++ b/routes/web.php @@ -251,6 +251,8 @@ Route::middleware('auth')->group(function () { Route::get('/settings/users/{id}', [UserControllers\UserController::class, 'edit']); Route::put('/settings/users/{id}', [UserControllers\UserController::class, 'update']); Route::delete('/settings/users/{id}', [UserControllers\UserController::class, 'destroy']); + Route::post('/settings/users/{id}/impersonate', [UserControllers\UserController::class, 'impersonate']); + Route::get('/impersonate/stop', [UserControllers\UserController::class, 'stopImpersonate']); // User Account Route::get('/my-account', [UserControllers\UserAccountController::class, 'redirect']);