Implement Imper.

This commit is contained in:
JonatanRek
2026-04-07 18:40:13 +02:00
parent e033578fea
commit 9589492081
9 changed files with 109 additions and 13 deletions

View File

@@ -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,

View File

@@ -0,0 +1,32 @@
<?php
namespace BookStack\Http\Middleware;
use BookStack\Permissions\Permission;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class Impersonate
{
/**
* Handle an incoming request.
*
* @param Closure(Request): (Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$impersonateId = session('impersonate', null);
if (empty($impersonateId)) {
return $next($request);
}
$realUser = auth()->user();
if ($realUser && $realUser->can(Permission::UsersManage)) {
Auth::onceUsingId($impersonateId);
}
return $next($request);
}
}

View File

@@ -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.
*

View File

@@ -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

View File

@@ -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.',

View File

@@ -54,6 +54,13 @@
@include('layouts.parts.base-body-start')
@include('layouts.parts.skip-to-content')
@include('layouts.parts.notifications')
@if(session('impersonate'))
<div style="background-color:#c0392b;color:#fff;text-align:center;padding:8px 16px;font-size:0.9em;">
{{ trans('settings.users_impersonating', ['name' => user()->name]) }}
&nbsp;|&nbsp;
<a href="{{ url('/impersonate/stop') }}" style="color:#fff;text-decoration:underline;">{{ trans('settings.users_impersonate_stop') }}</a>
</div>
@endif
@include('layouts.parts.header')
<div id="content" components="@yield('content-components')" class="block">

View File

@@ -39,20 +39,27 @@
</li>
<li role="presentation"><hr></li>
<li>
@php
$logoutPath = match (config('auth.method')) {
'saml2' => '/saml2/logout',
'oidc' => '/oidc/logout',
default => '/logout',
}
@endphp
<form action="{{ url($logoutPath) }}" method="post">
{{ csrf_field() }}
<button class="icon-item" role="menuitem" data-shortcut="logout">
@if(session('impersonate'))
<a href="{{ url('/impersonate/stop') }}" role="menuitem" class="icon-item">
@icon('logout')
<div>{{ trans('auth.logout') }}</div>
</button>
</form>
<div>{{ trans('settings.users_impersonate_stop') }}</div>
</a>
@else
@php
$logoutPath = match (config('auth.method')) {
'saml2' => '/saml2/logout',
'oidc' => '/oidc/logout',
default => '/logout',
}
@endphp
<form action="{{ url($logoutPath) }}" method="post">
{{ csrf_field() }}
<button class="icon-item" role="menuitem" data-shortcut="logout">
@icon('logout')
<div>{{ trans('auth.logout') }}</div>
</button>
</form>
@endif
</li>
</ul>
</div>

View File

@@ -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'))
<section class="card content-wrap auto-height">
<h2 class="list-heading">{{ trans('settings.users_impersonate') }}</h2>
<p class="text-muted text-small">{{ trans('settings.users_impersonate_desc') }}</p>
<form action="{{ url("/settings/users/{$user->id}/impersonate") }}" method="post">
{!! csrf_field() !!}
<button type="submit" class="button outline">{{ trans('settings.users_impersonate_action') }}</button>
</form>
</section>
@endif
</div>
@stop

View File

@@ -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']);