Skip to content

Commit 89909cf

Browse files
committed
feat: implement user impersonation
1 parent ba920c6 commit 89909cf

File tree

6 files changed

+139
-3
lines changed

6 files changed

+139
-3
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"pxlrbt/filament-excel": "^2.4",
6262
"spatie/laravel-package-tools": "^1.18",
6363
"spatie/laravel-translatable": "^6.11",
64+
"stechstudio/filament-impersonate": "^3.16",
6465
"tangodev-it/filament-emoji-picker": "^1.0",
6566
"typesense/typesense-php": "^5.0"
6667
},

src/Filament/Resources/UserResource.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use Illuminate\Database\Eloquent\SoftDeletingScope;
2525
use Illuminate\Support\Facades\Hash;
2626
use pxlrbt\FilamentExcel\Actions\Tables\ExportBulkAction;
27+
use STS\FilamentImpersonate\Tables\Actions\Impersonate;
2728

2829
class UserResource extends Resource implements HasShieldPermissions
2930
{
@@ -173,6 +174,9 @@ public static function table(Table $table): Table
173174
Tables\Actions\ActionGroup::make([
174175
Tables\Actions\ViewAction::make(),
175176
Tables\Actions\EditAction::make(),
177+
Impersonate::make()
178+
->grouped()
179+
->redirectTo(route('filament.admin.tenant')),
176180
Tables\Actions\DeleteAction::make()
177181
->authorize(fn (User $record) => auth()->user()->can('delete_user') && auth()->id() !== $record->id)
178182
->requiresConfirmation(),
@@ -305,6 +309,7 @@ public static function getPermissionPrefixes(): array
305309
'restore_any',
306310
'force_delete',
307311
'force_delete_any',
312+
'impersonate',
308313
];
309314
}
310315
}

src/Filament/Resources/UserResource/Pages/ViewUser.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Eclipse\Core\Filament\Resources\UserResource;
66
use Filament\Actions;
77
use Filament\Resources\Pages\ViewRecord;
8+
use STS\FilamentImpersonate\Pages\Actions\Impersonate;
89

910
class ViewUser extends ViewRecord
1011
{
@@ -14,6 +15,9 @@ protected function getHeaderActions(): array
1415
{
1516
return [
1617
Actions\EditAction::make(),
18+
Impersonate::make()
19+
->record($this->getRecord())
20+
->redirectTo(route('filament.admin.tenant')),
1721
];
1822
}
1923
}

src/Models/User.php

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Eclipse\Core\Models;
44

55
use Eclipse\Core\Database\Factories\UserFactory;
6+
use Exception;
67
use Filament\Models\Contracts\FilamentUser;
78
use Filament\Models\Contracts\HasAvatar;
89
use Filament\Models\Contracts\HasTenants;
@@ -119,7 +120,7 @@ protected static function booted()
119120

120121
static::retrieved(function (self $user) {
121122
if ($user->trashed() && auth()->check() && request()->routeIs('login')) {
122-
throw new \Exception('This account has been deactivated.');
123+
throw new Exception('This account has been deactivated.');
123124
}
124125
});
125126
}
@@ -139,14 +140,22 @@ public function updateLoginTracking()
139140
/**
140141
* Delete the user account, preventing self-deletion.
141142
*
142-
* @throws \Exception If the user attempts to delete their own account.
143+
* @throws Exception If the user attempts to delete their own account.
143144
*/
144145
public function delete(): ?bool
145146
{
146147
if ($this->id === auth()->id()) {
147-
throw new \Exception('You cannot delete your own account.');
148+
throw new Exception('You cannot delete your own account.');
148149
}
149150

150151
return parent::delete();
151152
}
153+
154+
/**
155+
* Determine if the user can impersonate other users.
156+
*/
157+
public function canImpersonate(): bool
158+
{
159+
return $this->can('impersonate', User::class);
160+
}
152161
}

src/Policies/UserPolicy.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,12 @@ public function forceDeleteAny(User $user): bool
9292
{
9393
return $user->can('force_delete_any_user');
9494
}
95+
96+
/**
97+
* Determine whether the user can impersonate other users.
98+
*/
99+
public function impersonate(User $user): bool
100+
{
101+
return $user->can('impersonate_user');
102+
}
95103
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?php
2+
3+
use Eclipse\Core\Models\User;
4+
use Illuminate\Auth\Access\AuthorizationException;
5+
use Illuminate\Support\Facades\Auth;
6+
use Illuminate\Support\Facades\Gate;
7+
use STS\FilamentImpersonate\Pages\Actions\Impersonate as ImpersonatePageAction;
8+
use STS\FilamentImpersonate\Tables\Actions\Impersonate as ImpersonateTableAction;
9+
10+
beforeEach(function () {
11+
$this->set_up_super_admin_and_tenant();
12+
13+
// Create a target user to impersonate
14+
$this->targetUser = User::factory()->create([
15+
'first_name' => 'Target',
16+
'last_name' => 'User',
17+
'email' => 'target@example.com',
18+
]);
19+
20+
// Create a user with impersonate permission
21+
$this->authorizedUser = User::factory()->create([
22+
'first_name' => 'Authorized',
23+
'last_name' => 'User',
24+
'email' => 'authorized@example.com',
25+
]);
26+
$this->authorizedUser->givePermissionTo('impersonate_user');
27+
28+
// Create a user without impersonate permission
29+
$this->unauthorizedUser = User::factory()->create([
30+
'first_name' => 'Unauthorized',
31+
'last_name' => 'User',
32+
'email' => 'unauthorized@example.com',
33+
]);
34+
});
35+
36+
test('non-authorized user cannot impersonate other users', function () {
37+
// Login as unauthorized user
38+
Auth::login($this->unauthorizedUser);
39+
40+
// Assert user doesn't have impersonate permission
41+
$this->assertFalse($this->unauthorizedUser->hasPermissionTo('impersonate_user'));
42+
$this->assertFalse($this->unauthorizedUser->can('impersonate', User::class));
43+
$this->assertFalse($this->unauthorizedUser->canImpersonate());
44+
45+
// Test authorization gate
46+
$this->expectException(AuthorizationException::class);
47+
Gate::authorize('impersonate', User::class);
48+
});
49+
50+
test('non-authorized user cannot see and trigger the impersonate table and page action', function () {
51+
// Login as unauthorized user
52+
Auth::login($this->unauthorizedUser);
53+
54+
// Assert user doesn't have impersonate permission
55+
$this->assertFalse($this->unauthorizedUser->hasPermissionTo('impersonate_user'));
56+
57+
// Create an instance of the Impersonate action
58+
$action = ImpersonateTableAction::make('impersonate')
59+
->record($this->targetUser);
60+
61+
// Assert the action is not authorized
62+
$this->assertFalse($action->isVisible());
63+
$this->assertFalse($action->isEnabled());
64+
65+
// Create an instance of the Impersonate page action
66+
$action = ImpersonatePageAction::make('impersonate')
67+
->record($this->targetUser);
68+
69+
// Assert the action is not authorized
70+
$this->assertFalse($action->isVisible());
71+
$this->assertFalse($action->isEnabled());
72+
});
73+
74+
test('authorized user can impersonate other users', function () {
75+
// Login as authorized user
76+
Auth::login($this->authorizedUser);
77+
78+
// Assert user has impersonate permission
79+
$this->assertTrue($this->authorizedUser->hasPermissionTo('impersonate_user'));
80+
$this->assertTrue($this->authorizedUser->can('impersonate', User::class));
81+
$this->assertTrue($this->authorizedUser->canImpersonate());
82+
83+
// Test authorization gate
84+
$this->assertTrue(Gate::allows('impersonate', User::class));
85+
});
86+
87+
test('authorized user can see and trigger the impersonate table and page action', function () {
88+
// Login as authorized user
89+
Auth::login($this->authorizedUser);
90+
91+
// Assert user has impersonate permission
92+
$this->assertTrue($this->authorizedUser->hasPermissionTo('impersonate_user'));
93+
94+
// Create an instance of the Impersonate action
95+
$action = ImpersonateTableAction::make('impersonate')
96+
->record($this->targetUser);
97+
98+
// Assert the action is authorized
99+
$this->assertTrue($action->isVisible());
100+
$this->assertTrue($action->isEnabled());
101+
102+
// Create an instance of the Impersonate page action
103+
$action = ImpersonatePageAction::make('impersonate')
104+
->record($this->targetUser);
105+
106+
// Assert the action is authorized
107+
$this->assertTrue($action->isVisible());
108+
$this->assertTrue($action->isEnabled());
109+
});

0 commit comments

Comments
 (0)