3
0
mirror of https://github.com/snipe/snipe-it.git synced 2026-08-18 03:06:23 +00:00

Superadmin: Added ability to login (impersonate) another user

This commit is contained in:
snipe
2026-07-07 20:49:19 +01:00
parent 3611fc9df5
commit 4675e4bbd9
20 changed files with 626 additions and 9 deletions

View File

@ -221,6 +221,11 @@ LOG_CHANNEL=single
LOG_DEPRECATIONS=false
LOG_MAX_DAYS=10
APP_LOCKED=false
# Comma-separated usernames allowed to impersonate other users (e.g. admin,jsmith).
# Users in this list must ALSO be superusers. Wrap usernames that contain commas
# in double quotes, e.g. ALLOW_USER_IMPERSONATION=admin,"jane, doe". Leave blank
# to disable entirely.
ALLOW_USER_IMPERSONATION=
APP_CIPHER=AES-256-CBC
APP_FORCE_TLS=false
APP_ALLOW_INSECURE_HOSTS=false

View File

@ -26,6 +26,8 @@ enum ActionType: string
case Merged = 'merged';
case TokenRevoked = 'token revoked';
case TokenUnrevoked = 'token unrevoked';
case Impersonated = 'impersonated';
case StoppedImpersonating = 'stopped impersonating';
// Licenses
case DeleteSeats = 'delete seats';

View File

@ -122,6 +122,10 @@ class IconHelper
return 'fa-solid fa-key';
case 'api-key':
return 'fas fa-user-secret';
case 'impersonate':
return 'fa-solid fa-person-walking-arrow-right';
case 'undo':
return 'fas fa-arrow-left';
case 'nav-toggle':
return 'fas fa-bars';
case 'dashboard':

View File

@ -54,7 +54,23 @@ class SettingsController extends Controller
{
$settings = Setting::getSettings();
return view('settings/index', compact('settings'));
$impersonationUsernames = (array) config('app.user_impersonation_usernames');
if (empty($impersonationUsernames)) {
$impersonators = collect();
$missingImpersonationUsernames = [];
} else {
$impersonators = User::withTrashed()
->whereIn(DB::raw('LOWER(username)'), array_map('mb_strtolower', $impersonationUsernames))
->orderBy('username')
->get();
$foundLower = $impersonators->map(fn ($u) => mb_strtolower((string) $u->username))->all();
$missingImpersonationUsernames = array_values(array_filter(
$impersonationUsernames,
fn ($name) => ! in_array(mb_strtolower($name), $foundLower, true)
));
}
return view('settings/index', compact('settings', 'impersonators', 'missingImpersonationUsernames'));
}
/**

View File

@ -0,0 +1,92 @@
<?php
namespace App\Http\Controllers\Users;
use App\Http\Controllers\Controller;
use App\Models\Actionlog;
use App\Models\CompanyableScope;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class ImpersonateController extends Controller
{
public function start(Request $request, User $user): RedirectResponse
{
$actor = Auth::user();
if (empty(config('app.user_impersonation_usernames'))) {
abort(404);
}
if (! $actor || ! $actor->canImpersonate()) {
abort(403);
}
if ($user->id === $actor->id) {
return redirect()->route('users.show', $user)
->with('error', trans('admin/users/message.impersonate.cannot_impersonate_self'));
}
if ($user->deleted_at !== null || $user->activated != 1) {
return redirect()->route('users.show', $user)
->with('error', trans('admin/users/message.impersonate.target_not_active'));
}
$log = new Actionlog;
$log->item_type = User::class;
$log->item_id = $user->id;
$log->target_type = User::class;
$log->target_id = $user->id;
$log->created_at = date('Y-m-d H:i:s');
$log->created_by = $actor->id;
$log->logaction('impersonated');
$impersonatorId = $actor->id;
Auth::login($user);
$request->session()->put('impersonator_id', $impersonatorId);
return redirect()->route('home')
->with('success', trans('admin/users/message.impersonate.started', ['name' => $user->display_name]));
}
public function stop(Request $request): RedirectResponse
{
$impersonatorId = $request->session()->pull('impersonator_id');
if (! $impersonatorId) {
return redirect()->route('home');
}
$impersonatedId = Auth::id();
// Bypass CompanyableScope: the impersonated user may not share a company with the
// original superuser, but we still need to restore their session.
$impersonator = User::withTrashed()
->withoutGlobalScope(CompanyableScope::class)
->find($impersonatorId);
if (! $impersonator) {
Auth::logout();
return redirect()->route('login')
->with('error', trans('admin/users/message.impersonate.impersonator_missing'));
}
if ($impersonatedId) {
$log = new Actionlog;
$log->item_type = User::class;
$log->item_id = $impersonatedId;
$log->target_type = User::class;
$log->target_id = $impersonatedId;
$log->created_at = date('Y-m-d H:i:s');
$log->created_by = $impersonator->id;
$log->logaction('stopped impersonating');
}
Auth::login($impersonator);
return redirect()->route('users.show', $impersonatedId ?: $impersonator->id)
->with('success', trans('admin/users/message.impersonate.stopped'));
}
}

View File

@ -552,6 +552,17 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
return $this->checkPermissionSection('superuser');
}
public function canImpersonate(): bool
{
if (! $this->isSuperUser()) {
return false;
}
$allowed = array_map('mb_strtolower', (array) config('app.user_impersonation_usernames'));
return in_array(mb_strtolower((string) $this->username), $allowed, true);
}
/**
* Checks if the user is an admin
*

View File

@ -23,6 +23,7 @@ use App\Observers\LocationObserver;
use App\Observers\MaintenanceObserver;
use App\Observers\SettingObserver;
use App\Observers\UserObserver;
use App\View\Composers\ImpersonationBannerComposer;
use App\View\Composers\SidebarComposer;
use Illuminate\Pagination\Paginator;
use Illuminate\Routing\UrlGenerator;
@ -78,6 +79,7 @@ class AppServiceProvider extends ServiceProvider
Paginator::useBootstrap();
View::composer('layouts.default', SidebarComposer::class);
View::composer('partials.impersonation-banner', ImpersonationBannerComposer::class);
Schema::defaultStringLength(191);
Accessory::observe(AccessoryObserver::class);

View File

@ -0,0 +1,28 @@
<?php
namespace App\View\Composers;
use App\Models\CompanyableScope;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Session;
use Illuminate\View\View;
class ImpersonationBannerComposer
{
public function compose(View $view): void
{
$impersonatorId = Session::get('impersonator_id');
$impersonator = null;
if ($impersonatorId && Auth::check()) {
// Bypass CompanyableScope: the impersonator is stored by their own action in start(),
// so looking them up must not be filtered by the impersonated user's company view.
$impersonator = User::withTrashed()
->withoutGlobalScope(CompanyableScope::class)
->find($impersonatorId);
}
$view->with('impersonator', $impersonator);
}
}

View File

@ -357,6 +357,41 @@ return [
'lock_passwords' => env('APP_LOCKED', false),
/*
|--------------------------------------------------------------------------
| Superuser Impersonation
|--------------------------------------------------------------------------
|
| Comma-separated list of usernames allowed to impersonate other users.
| Users in this list must ALSO be superusers. Empty or unset means the
| feature is completely off. Wrap usernames that contain commas in double
| quotes. Example: ALLOW_USER_IMPERSONATION=admin,"jane, doe"
|
*/
'user_impersonation_usernames' => (function () {
$raw = env('ALLOW_USER_IMPERSONATION', '');
// Reject anything that isn't a string. env() converts literal "true"/"false"/"null"/etc.
// to PHP bool/null, which would otherwise stringify to garbage tokens.
if (! is_string($raw) || trim($raw) === '') {
return [];
}
$usernames = [];
$seen = [];
foreach (str_getcsv($raw, ',', '"', '\\') as $token) {
$token = trim((string) $token);
$lower = mb_strtolower($token);
if ($token !== '' && ! isset($seen[$lower])) {
$usernames[] = $token;
$seen[$lower] = true;
}
}
return $usernames;
})(),
/*
|--------------------------------------------------------------------------
| Minimum PHP version

View File

@ -444,6 +444,12 @@ return [
'mail_from' => 'Mail From Address',
'database_driver' => 'Database Driver',
'bs_table_storage' => 'Table Storage',
'user_impersonation' => 'User Impersonation',
'user_impersonation_disabled' => 'Disabled. Set ALLOW_USER_IMPERSONATION in your .env to a comma-separated list of usernames.',
'user_impersonation_not_superuser' => 'Not a superuser',
'user_impersonation_not_superuser_help' => 'This user is in the impersonation allowlist but is not a superuser, so they cannot actually impersonate anyone. Remove them from the .env or grant superuser access.',
'user_impersonation_missing' => 'User not found',
'user_impersonation_deactivated' => 'Deactivated',
'timezone' => 'Timezone',
'test_mail' => 'Test Mail',
'profile_edit' => 'Edit Profile',

View File

@ -65,4 +65,8 @@ return [
'no_companies_assigned' => '(No companies assigned)',
'cannot_edit_privileged_user_companies' => 'Only an admin or super admin can modify the company assignments of an admin or super admin user.',
'cannot_manage_companies_without_membership' => 'Full multiple company support with floater mode is enabled, so you must be assigned to at least one company before you can manage another user\'s company assignments.',
'impersonate_user' => 'Login as :name',
'impersonating_banner_title' => 'Impersonating:',
'impersonating_banner_text' => 'You are currently logged in as :name. Anything you do will be recorded as if :name did it. Your real account is :impersonator.',
'impersonating_stop_link' => 'Switch back to :name',
];

View File

@ -20,6 +20,14 @@ return [
'user_has_no_email' => 'This user does not have an email address in their profile.',
'log_record_not_found' => 'A matching log record for this user could not be found.',
'impersonate' => [
'started' => 'You are now logged in as :name.',
'stopped' => 'You are back to your own account.',
'cannot_impersonate_self' => 'You cannot log in as yourself.',
'target_not_active' => 'That user is deactivated or deleted and cannot be logged in as.',
'impersonator_missing' => 'The original account for this impersonation session no longer exists. Please log in again.',
],
'success' => [
'create' => 'User was successfully created.',
'update' => 'User was successfully updated.',

View File

@ -281,6 +281,9 @@ return [
'reports' => 'Reports',
'restored' => 'restored',
'restore' => 'Restore',
'impersonated' => 'Logged in as',
'impersonate' => 'Login as This User',
'stopped_impersonating' => 'Stopped impersonating',
'requestable_models' => 'Requestable Models',
'requestable_items' => 'Requestable Items',
'requestable' => 'Requestable',

View File

@ -1939,6 +1939,8 @@
<div class="content-wrapper" role="main" id="setting-list">
@include('partials.impersonation-banner')
@if ($debug_in_production)
<div class="row" style="margin-bottom: 0px; background-color: red; color: white; font-size: 15px;">
<div class="col-md-12"

View File

@ -0,0 +1,16 @@
@if (!empty($impersonator) && Auth::check())
<div class="row" role="alert" aria-live="polite" style="margin-bottom: 0px;">
<div class="col-md-12" style="background-color: #b94a48; color: #ffffff; padding: 14px 30px 14px 30px; font-size: 17px;">
<x-icon type="impersonate" class="pull-left" style="margin-right: 15px; margin-top: 2px;"/>
<strong>{{ trans('admin/users/general.impersonating_banner_title') }}</strong>
{{ trans('admin/users/general.impersonating_banner_text', ['name' => Auth::user()->display_name, 'impersonator' => $impersonator->display_name]) }}
<form action="{{ route('users.impersonate.stop') }}" method="POST" class="form-inline pull-right" style="display: inline;">
{{ csrf_field() }}
<button type="submit" class="btn btn-sm btn-default" style="background-color: #ffffff; color: #b94a48; border-color: #ffffff; font-weight: bold;">
<x-icon type="undo" class="fa-fw"/>
{{ trans('admin/users/general.impersonating_stop_link', ['name' => $impersonator->display_name]) }}
</button>
</form>
</div>
</div>
@endif

View File

@ -455,6 +455,43 @@
<div class="col-md-4">
</div>
</div>
<!-- row -->
<div class="row">
<div class="col-md-2">
<strong>{{ trans('admin/settings/general.user_impersonation') }}:</strong>
</div>
<div class="col-md-10">
@if ($impersonators->isEmpty() && empty($missingImpersonationUsernames))
<em class="text-muted">{{ trans('admin/settings/general.user_impersonation_disabled') }}</em>
@else
<ul>
@foreach ($impersonators as $impersonator)
<li>
<a href="{{ route('users.show', $impersonator->id) }}">{{ $impersonator->display_name }}</a>
<code>{{ $impersonator->username }}</code>
@if (! $impersonator->isSuperUser())
<span class="label label-warning" data-tooltip="true" title="{{ trans('admin/settings/general.user_impersonation_not_superuser_help') }}">
{{ trans('admin/settings/general.user_impersonation_not_superuser') }}
</span>
@endif
@if ($impersonator->deleted_at !== null)
<span class="label label-danger">{{ trans('general.deleted') }}</span>
@elseif ($impersonator->activated != 1)
<span class="label label-default">{{ trans('admin/settings/general.user_impersonation_deactivated') }}</span>
@endif
</li>
@endforeach
@foreach ($missingImpersonationUsernames as $missingUsername)
<li>
<code>{{ $missingUsername }}</code>
<span class="label label-danger">{{ trans('admin/settings/general.user_impersonation_missing') }}</span>
</li>
@endforeach
</ul>
@endif
</div>
</div>
</div>
</div>
</div>

View File

@ -292,11 +292,25 @@
</x-well>
<!-- Impersonation button -->
@if (Auth::check() && Auth::user()->canImpersonate() && ($user->id !== Auth::id()) && ($user->deleted_at === null) && ($user->activated == 1))
<form action="{{ route('users.impersonate.start', $user->id) }}" method="POST" class="form-inline" style="display: inline;">
{{ csrf_field() }}
<button type="submit" class="btn btn-danger hidden-print btn-social btn-block" data-tooltip="true" data-title="{{ trans('admin/users/general.impersonate_user', ['name' => $user->display_name]) }}">
<x-icon type="impersonate" class="fa-fw" style="font-size: 17px;"/>
{{ trans('general.impersonate') }}
<span class="sr-only">{{ trans('admin/users/general.impersonate_user', ['name' => $user->display_name]) }}</span>
</button>
</form>
@endif
@if ( ($user->activated == '1') && (auth()->user()->isSuperUser()) && ($user->two_factor_active_and_enrolled()) && ($snipeSettings->two_factor_enabled!='0') && ($snipeSettings->two_factor_enabled!=''))
<!-- 2FA reset -->
<a class="btn btn-theme btn-sm" id="two_factor_reset" style="margin-right: 10px; margin-top: 10px;">
<a class="btn btn-theme hidden-print btn-social btn-block" id="two_factor_reset" style="margin-right: 10px; margin-top: 10px;">
<x-icon type="mobile" class="fa-fw"/>
{{ trans('admin/settings/general.two_factor_reset') }}
</a>
<span id="two_factor_reseticon">
@ -312,11 +326,6 @@
@endif
@if ($snipeSettings->isQrEnabled())
<div class="col-md-12 text-center user-qr-img" style="padding-top: 15px;">
<img src="{{ route('qr_code/common', ['object_type' => 'users', 'id' => $user->id]) }}" class="img-thumbnail" style="height: 150px; width: 150px; margin-right: 10px;" alt="QR code for {{ $user->display_name }}">
</div>
@endif
</x-page-column>
<!-- end side stats well column-->
@ -543,7 +552,7 @@
<x-table.files object_type="users" :object="$user"/>
</x-tabs.pane>
<x-tabs.pane name="eulas" :count="$user->accessories()->count()">
<x-tabs.pane name="eulas" :count="$user->eulas()->count()">
<x-slot:table_header>
{{ trans('general.eula') }}
</x-slot:table_header>
@ -611,7 +620,8 @@
<x-button.clone :item="$user" :route="route('users.clone.show', $user)"/>
<x-button.restore :item="$user" :route="route('users.restore.store', $user)"/>
@if($user->allAssignedCount() != '0')
@if($user->allAssignedCount() != '0')
<a href="{{ route('users.print', $user->id) }}" class="btn btn-sm btn-theme hidden-print" target="_blank" rel="noopener" data-tooltip="true" data-title="{{ trans('admin/users/general.print_assigned') }}">
<x-icon type="print" class="fa-fw"/>
</a>

View File

@ -90,6 +90,22 @@ Route::group(['prefix' => 'users', 'middleware' => ['auth']], function () {
]
)->name('users.acceptance_reminder')->withTrashed();
Route::post(
'{user}/impersonate',
[
Users\ImpersonateController::class,
'start',
]
)->name('users.impersonate.start');
Route::post(
'impersonate/stop',
[
Users\ImpersonateController::class,
'stop',
]
)->name('users.impersonate.stop');
Route::post(
'bulkedit',
[

View File

@ -0,0 +1,79 @@
<?php
namespace Tests\Feature\Settings;
use App\Models\User;
use Tests\TestCase;
class ImpersonationSettingsRowTest extends TestCase
{
public function test_row_shows_disabled_state_when_no_usernames_configured()
{
config(['app.user_impersonation_usernames' => []]);
$admin = User::factory()->superuser()->create();
$this->actingAs($admin)
->get(route('settings.index'))
->assertOk()
->assertSee(trans('admin/settings/general.user_impersonation'))
->assertSee(trans('admin/settings/general.user_impersonation_disabled'));
}
public function test_row_lists_a_valid_superuser_without_warnings()
{
$allowed = User::factory()->superuser()->create(['username' => 'allowed_super']);
config(['app.user_impersonation_usernames' => [$allowed->username]]);
$admin = User::factory()->superuser()->create();
$this->actingAs($admin)
->get(route('settings.index'))
->assertOk()
->assertSee($allowed->display_name)
->assertSee($allowed->username)
->assertDontSee(trans('admin/settings/general.user_impersonation_not_superuser'))
->assertDontSee(trans('admin/settings/general.user_impersonation_missing'));
}
public function test_row_flags_non_superuser_in_allowlist()
{
$notSuper = User::factory()->admin()->create(['username' => 'not_super']);
config(['app.user_impersonation_usernames' => [$notSuper->username]]);
$admin = User::factory()->superuser()->create();
$this->actingAs($admin)
->get(route('settings.index'))
->assertOk()
->assertSee($notSuper->display_name)
->assertSee(trans('admin/settings/general.user_impersonation_not_superuser'));
}
public function test_row_flags_missing_username()
{
config(['app.user_impersonation_usernames' => ['ghost_admin']]);
$admin = User::factory()->superuser()->create();
$this->actingAs($admin)
->get(route('settings.index'))
->assertOk()
->assertSee('ghost_admin')
->assertSee(trans('admin/settings/general.user_impersonation_missing'));
}
public function test_row_case_insensitively_matches_usernames()
{
$allowed = User::factory()->superuser()->create(['username' => 'SnipeAdmin']);
config(['app.user_impersonation_usernames' => ['snipeadmin']]);
$admin = User::factory()->superuser()->create();
$this->actingAs($admin)
->get(route('settings.index'))
->assertOk()
->assertSee($allowed->display_name)
->assertDontSee(trans('admin/settings/general.user_impersonation_missing'));
}
}

View File

@ -0,0 +1,241 @@
<?php
namespace Tests\Feature\Users;
use App\Models\Company;
use App\Models\User;
use Tests\TestCase;
class ImpersonateUserTest extends TestCase
{
protected function allow(User ...$users): void
{
config(['app.user_impersonation_usernames' => array_map(fn ($u) => $u->username, $users)]);
}
public function test_impersonate_endpoint_is_disabled_when_list_is_empty()
{
config(['app.user_impersonation_usernames' => []]);
$actor = User::factory()->superuser()->create();
$target = User::factory()->create(['activated' => 1]);
$this->actingAs($actor)
->post(route('users.impersonate.start', $target))
->assertNotFound();
$this->assertNull(session('impersonator_id'));
}
public function test_non_superuser_cannot_impersonate_even_if_id_is_in_list()
{
$actor = User::factory()->admin()->create();
$target = User::factory()->create(['activated' => 1]);
$this->allow($actor);
$this->actingAs($actor)
->post(route('users.impersonate.start', $target))
->assertForbidden();
$this->assertNull(session('impersonator_id'));
}
public function test_superuser_not_in_allowlist_cannot_impersonate()
{
$actor = User::factory()->superuser()->create();
$someoneElse = User::factory()->superuser()->create();
$target = User::factory()->create(['activated' => 1]);
$this->allow($someoneElse);
$this->actingAs($actor)
->post(route('users.impersonate.start', $target))
->assertForbidden();
$this->assertNull(session('impersonator_id'));
}
public function test_allowlisted_superuser_can_impersonate_activated_user()
{
$actor = User::factory()->superuser()->create();
$target = User::factory()->create(['activated' => 1]);
$this->allow($actor);
$response = $this->actingAs($actor)
->post(route('users.impersonate.start', $target));
$response->assertRedirect(route('home'));
$this->assertSame($target->id, auth()->id());
$this->assertSame($actor->id, session('impersonator_id'));
$this->assertDatabaseHas('action_logs', [
'item_type' => User::class,
'item_id' => $target->id,
'created_by' => $actor->id,
'action_type' => 'impersonated',
]);
}
public function test_allowlisted_superuser_cannot_impersonate_deactivated_user()
{
$actor = User::factory()->superuser()->create();
$target = User::factory()->create(['activated' => 0]);
$this->allow($actor);
$this->actingAs($actor)
->post(route('users.impersonate.start', $target))
->assertRedirect(route('users.show', $target));
$this->assertSame($actor->id, auth()->id());
$this->assertNull(session('impersonator_id'));
}
public function test_allowlisted_superuser_cannot_impersonate_themselves()
{
$actor = User::factory()->superuser()->create();
$this->allow($actor);
$this->actingAs($actor)
->post(route('users.impersonate.start', $actor))
->assertRedirect(route('users.show', $actor));
$this->assertNull(session('impersonator_id'));
}
public function test_stop_impersonation_restores_original_user()
{
$actor = User::factory()->superuser()->create();
$target = User::factory()->create(['activated' => 1]);
$this->allow($actor);
$this->actingAs($actor)
->post(route('users.impersonate.start', $target))
->assertRedirect(route('home'));
$this->assertSame($target->id, auth()->id());
$stop = $this->post(route('users.impersonate.stop'));
$stop->assertRedirect(route('users.show', $target));
$this->assertSame($actor->id, auth()->id());
$this->assertNull(session('impersonator_id'));
$this->assertDatabaseHas('action_logs', [
'item_type' => User::class,
'item_id' => $target->id,
'created_by' => $actor->id,
'action_type' => 'stopped impersonating',
]);
}
public function test_banner_is_visible_after_impersonating_a_non_admin()
{
$actor = User::factory()->superuser()->create(['first_name' => 'Sooper', 'last_name' => 'Actor']);
$target = User::factory()->create(['activated' => 1, 'first_name' => 'Target', 'last_name' => 'User']);
$this->allow($actor);
$this->actingAs($actor)
->post(route('users.impersonate.start', $target))
->assertRedirect(route('home'));
$this->assertSame($actor->id, session('impersonator_id'));
$follow = $this->followingRedirects()->get(route('home'));
$follow->assertOk()
->assertSee(trans('admin/users/general.impersonating_banner_title'))
->assertSee(route('users.impersonate.stop'), false);
}
public function test_banner_and_stop_work_across_company_scoping()
{
$this->settings->enableMultipleFullCompanySupport();
[$companyA, $companyB] = Company::factory()->count(2)->create();
$actor = User::factory()->superuser()->create();
$actor->companies()->sync([$companyA->id]);
$target = User::factory()->create(['activated' => 1, 'company_id' => $companyB->id]);
$target->companies()->sync([$companyB->id]);
$this->allow($actor);
$this->actingAs($actor)
->post(route('users.impersonate.start', $target))
->assertRedirect(route('home'));
$this->assertSame($target->id, auth()->id());
$this->assertSame($actor->id, session('impersonator_id'));
$follow = $this->followingRedirects()->get(route('home'));
$follow->assertOk()
->assertSee(trans('admin/users/general.impersonating_banner_title'))
->assertSee(route('users.impersonate.stop'), false);
$this->post(route('users.impersonate.stop'))
->assertRedirect(route('users.show', $target));
$this->assertSame($actor->id, auth()->id());
}
public function test_stop_impersonation_no_op_when_not_impersonating()
{
$actor = User::factory()->create();
$this->actingAs($actor)
->post(route('users.impersonate.stop'))
->assertRedirect(route('home'));
$this->assertSame($actor->id, auth()->id());
}
public function test_button_hidden_when_list_is_empty()
{
config(['app.user_impersonation_usernames' => []]);
$actor = User::factory()->superuser()->create();
$target = User::factory()->create(['activated' => 1]);
$this->actingAs($actor)
->get(route('users.show', $target))
->assertOk()
->assertDontSee(route('users.impersonate.start', $target));
}
public function test_button_visible_to_allowlisted_superuser()
{
$actor = User::factory()->superuser()->create();
$target = User::factory()->create(['activated' => 1]);
$this->allow($actor);
$this->actingAs($actor)
->get(route('users.show', $target))
->assertOk()
->assertSee(route('users.impersonate.start', $target), false);
}
public function test_button_hidden_from_non_allowlisted_superuser()
{
$actor = User::factory()->superuser()->create();
$someoneElse = User::factory()->superuser()->create();
$target = User::factory()->create(['activated' => 1]);
$this->allow($someoneElse);
$this->actingAs($actor)
->get(route('users.show', $target))
->assertOk()
->assertDontSee(route('users.impersonate.start', $target));
}
public function test_button_hidden_from_non_superuser_in_allowlist()
{
$actor = User::factory()->admin()->create();
$target = User::factory()->create(['activated' => 1]);
$this->allow($actor);
$this->actingAs($actor)
->get(route('users.show', $target))
->assertOk()
->assertDontSee(route('users.impersonate.start', $target));
}
}