Skip to content

Commit 9589492

Browse files
committed
Implement Imper.
1 parent e033578 commit 9589492

File tree

9 files changed

+109
-13
lines changed

9 files changed

+109
-13
lines changed

app/Http/Kernel.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class Kernel extends HttpKernel
3636
\BookStack\Http\Middleware\CheckEmailConfirmed::class,
3737
\BookStack\Http\Middleware\RunThemeActions::class,
3838
\BookStack\Http\Middleware\Localization::class,
39+
\BookStack\Http\Middleware\Impersonate::class,
3940
],
4041
'api' => [
4142
\BookStack\Http\Middleware\ThrottleApiRequests::class,
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
namespace BookStack\Http\Middleware;
4+
5+
use BookStack\Permissions\Permission;
6+
use Closure;
7+
use Illuminate\Http\Request;
8+
use Illuminate\Support\Facades\Auth;
9+
use Symfony\Component\HttpFoundation\Response;
10+
11+
class Impersonate
12+
{
13+
/**
14+
* Handle an incoming request.
15+
*
16+
* @param Closure(Request): (Response) $next
17+
*/
18+
public function handle(Request $request, Closure $next): Response
19+
{
20+
$impersonateId = session('impersonate', null);
21+
if (empty($impersonateId)) {
22+
return $next($request);
23+
}
24+
25+
$realUser = auth()->user();
26+
if ($realUser && $realUser->can(Permission::UsersManage)) {
27+
Auth::onceUsingId($impersonateId);
28+
}
29+
30+
return $next($request);
31+
}
32+
}

app/Users/Controllers/UserController.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,36 @@ public function delete(int $id)
191191
return view('users.delete', ['user' => $user]);
192192
}
193193

194+
/**
195+
* Start impersonating the specified user.
196+
*/
197+
public function impersonate(int $id)
198+
{
199+
$this->checkPermission(Permission::UsersManage);
200+
201+
$user = $this->userRepo->getById($id);
202+
203+
if ($user->isGuest() || $user->id === user()->id) {
204+
$this->showErrorNotification(trans('errors.users_cannot_impersonate'));
205+
return redirect("/settings/users/{$id}");
206+
}
207+
208+
session(['impersonate' => $user->id]);
209+
210+
return redirect('/');
211+
}
212+
213+
/**
214+
* Stop impersonating and return to user edit page.
215+
*/
216+
public function stopImpersonate()
217+
{
218+
$userId = session('impersonate');
219+
session()->forget('impersonate');
220+
221+
return redirect("/settings/users/{$userId}");
222+
}
223+
194224
/**
195225
* Remove the specified user from storage.
196226
*

lang/en/errors.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
// Users
7979
'users_cannot_delete_only_admin' => 'You cannot delete the only admin',
8080
'users_cannot_delete_guest' => 'You cannot delete the guest user',
81+
'users_cannot_impersonate' => 'You cannot impersonate this user',
8182
'users_could_not_send_invite' => 'Could not create user since invite email failed to send',
8283

8384
// Roles

lang/en/settings.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,11 @@
231231
'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.',
232232
'users_password_warning' => 'Only fill the below if you would like to change the password for this user.',
233233
'users_system_public' => 'This user represents any guest users that visit your instance. It cannot be used to log in but is assigned automatically.',
234+
'users_impersonate' => 'Impersonate User',
235+
'users_impersonate_desc' => 'Log in and browse the application as this user.',
236+
'users_impersonate_action' => 'Impersonate',
237+
'users_impersonating' => 'Impersonating: :name',
238+
'users_impersonate_stop' => 'Stop Impersonating',
234239
'users_delete' => 'Delete User',
235240
'users_delete_named' => 'Delete user :userName',
236241
'users_delete_warning' => 'This will fully delete this user with the name \':userName\' from the system.',

resources/views/layouts/base.blade.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ class="@stack('body-class')">
5454
@include('layouts.parts.base-body-start')
5555
@include('layouts.parts.skip-to-content')
5656
@include('layouts.parts.notifications')
57+
@if(session('impersonate'))
58+
<div style="background-color:#c0392b;color:#fff;text-align:center;padding:8px 16px;font-size:0.9em;">
59+
{{ trans('settings.users_impersonating', ['name' => user()->name]) }}
60+
&nbsp;|&nbsp;
61+
<a href="{{ url('/impersonate/stop') }}" style="color:#fff;text-decoration:underline;">{{ trans('settings.users_impersonate_stop') }}</a>
62+
</div>
63+
@endif
5764
@include('layouts.parts.header')
5865

5966
<div id="content" components="@yield('content-components')" class="block">

resources/views/layouts/parts/header-user-menu.blade.php

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,20 +39,27 @@ class="icon-item">
3939
</li>
4040
<li role="presentation"><hr></li>
4141
<li>
42-
@php
43-
$logoutPath = match (config('auth.method')) {
44-
'saml2' => '/saml2/logout',
45-
'oidc' => '/oidc/logout',
46-
default => '/logout',
47-
}
48-
@endphp
49-
<form action="{{ url($logoutPath) }}" method="post">
50-
{{ csrf_field() }}
51-
<button class="icon-item" role="menuitem" data-shortcut="logout">
42+
@if(session('impersonate'))
43+
<a href="{{ url('/impersonate/stop') }}" role="menuitem" class="icon-item">
5244
@icon('logout')
53-
<div>{{ trans('auth.logout') }}</div>
54-
</button>
55-
</form>
45+
<div>{{ trans('settings.users_impersonate_stop') }}</div>
46+
</a>
47+
@else
48+
@php
49+
$logoutPath = match (config('auth.method')) {
50+
'saml2' => '/saml2/logout',
51+
'oidc' => '/oidc/logout',
52+
default => '/logout',
53+
}
54+
@endphp
55+
<form action="{{ url($logoutPath) }}" method="post">
56+
{{ csrf_field() }}
57+
<button class="icon-item" role="menuitem" data-shortcut="logout">
58+
@icon('logout')
59+
<div>{{ trans('auth.logout') }}</div>
60+
</button>
61+
</form>
62+
@endif
5663
</li>
5764
</ul>
5865
</div>

resources/views/users/edit.blade.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,17 @@ class="button outline">{{ trans('settings.users_mfa_configure') }}</a>
103103
@endif
104104

105105
@include('users.api-tokens.parts.list', ['user' => $user, 'context' => 'settings'])
106+
107+
@if(!$user->isGuest() && $user->id !== user()->id && userCan('users-manage') && !session('impersonate'))
108+
<section class="card content-wrap auto-height">
109+
<h2 class="list-heading">{{ trans('settings.users_impersonate') }}</h2>
110+
<p class="text-muted text-small">{{ trans('settings.users_impersonate_desc') }}</p>
111+
<form action="{{ url("/settings/users/{$user->id}/impersonate") }}" method="post">
112+
{!! csrf_field() !!}
113+
<button type="submit" class="button outline">{{ trans('settings.users_impersonate_action') }}</button>
114+
</form>
115+
</section>
116+
@endif
106117
</div>
107118

108119
@stop

routes/web.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,8 @@
251251
Route::get('/settings/users/{id}', [UserControllers\UserController::class, 'edit']);
252252
Route::put('/settings/users/{id}', [UserControllers\UserController::class, 'update']);
253253
Route::delete('/settings/users/{id}', [UserControllers\UserController::class, 'destroy']);
254+
Route::post('/settings/users/{id}/impersonate', [UserControllers\UserController::class, 'impersonate']);
255+
Route::get('/impersonate/stop', [UserControllers\UserController::class, 'stopImpersonate']);
254256

255257
// User Account
256258
Route::get('/my-account', [UserControllers\UserAccountController::class, 'redirect']);

0 commit comments

Comments
 (0)