Skip to content

Commit df5474d

Browse files
ryanmitchellclaudejasonvarga
authored
[6.x] Allow control over who can be impersonated in UserPolicy (#14469)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Jason Varga <jason@pixelfear.com>
1 parent f5195a4 commit df5474d

File tree

3 files changed

+101
-2
lines changed

3 files changed

+101
-2
lines changed

src/Actions/Impersonate.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ public function visibleTo($item)
2424
return false;
2525
}
2626

27-
return $item instanceof UserContract && $item->id() != User::current()->id();
27+
if (! ($item instanceof UserContract && $item->id() != User::current()->id())) {
28+
return false;
29+
}
30+
31+
return User::current()->can('impersonate', $item);
2832
}
2933

3034
public function visibleToBulk($items)
@@ -34,7 +38,7 @@ public function visibleToBulk($items)
3438

3539
public function authorize($authed, $user)
3640
{
37-
return $authed->can('impersonate users');
41+
return $authed->can('impersonate', $user);
3842
}
3943

4044
public function run($users, $values)

src/Policies/UserPolicy.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,11 @@ public function sendPasswordReset($authed, $user)
6767
{
6868
return $this->edit($authed, $user);
6969
}
70+
71+
public function impersonate($authed, $user)
72+
{
73+
$authed = User::fromUser($authed);
74+
75+
return $authed->hasPermission('impersonate users');
76+
}
7077
}

tests/Actions/ImpersonateTest.php

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,22 @@
22

33
namespace Tests\Actions;
44

5+
use Illuminate\Support\Facades\Gate;
56
use PHPUnit\Framework\Attributes\Group;
67
use PHPUnit\Framework\Attributes\Test;
8+
use Statamic\Actions\Impersonate as Action;
79
use Statamic\Facades\User;
10+
use Statamic\Policies\UserPolicy;
811
use Tests\ElevatesSessions;
12+
use Tests\FakesRoles;
913
use Tests\PreventSavingStacheItemsToDisk;
1014
use Tests\TestCase;
1115

1216
#[Group('elevated-session')]
1317
class ImpersonateTest extends TestCase
1418
{
1519
use ElevatesSessions;
20+
use FakesRoles;
1621
use PreventSavingStacheItemsToDisk;
1722

1823
private function impersonate($user)
@@ -41,4 +46,87 @@ public function it_authenticates_as_another_user_and_clears_elevated_session()
4146
$this->assertEquals($impersonated->id(), auth()->id());
4247
$this->assertFalse(request()->hasElevatedSession());
4348
}
49+
50+
#[Test]
51+
public function it_is_visible_to_a_valid_target_user()
52+
{
53+
$impersonator = tap(User::make()->email('admin@example.com')->makeSuper())->save();
54+
$impersonated = tap(User::make()->email('user@example.com'))->save();
55+
56+
$this->actingAs($impersonator);
57+
58+
$this->assertTrue((new Action)->visibleTo($impersonated));
59+
}
60+
61+
#[Test]
62+
public function it_is_not_visible_when_policy_denies_impersonation()
63+
{
64+
$this->setTestRoles(['impersonator' => ['impersonate users']]);
65+
66+
$impersonator = tap(User::make()->email('admin@example.com')->assignRole('impersonator'))->save();
67+
$impersonated = tap(User::make()->email('user@example.com'))->save();
68+
69+
Gate::policy(get_class($impersonated), DenyImpersonationPolicy::class);
70+
71+
$this->actingAs($impersonator);
72+
73+
$this->assertFalse((new Action)->visibleTo($impersonated));
74+
}
75+
76+
#[Test]
77+
public function it_is_authorized_with_the_default_policy()
78+
{
79+
$this->setTestRoles(['impersonator' => ['impersonate users']]);
80+
81+
$impersonator = tap(User::make()->email('admin@example.com')->assignRole('impersonator'))->save();
82+
$impersonated = tap(User::make()->email('user@example.com'))->save();
83+
84+
$this->assertTrue((new Action)->authorize($impersonator, $impersonated));
85+
}
86+
87+
#[Test]
88+
public function it_is_not_authorized_when_policy_denies_impersonation()
89+
{
90+
$this->setTestRoles(['impersonator' => ['impersonate users']]);
91+
92+
$impersonator = tap(User::make()->email('admin@example.com')->assignRole('impersonator'))->save();
93+
$impersonated = tap(User::make()->email('user@example.com'))->save();
94+
95+
Gate::policy(get_class($impersonated), DenyImpersonationPolicy::class);
96+
97+
$this->assertFalse((new Action)->authorize($impersonator, $impersonated));
98+
}
99+
100+
#[Test]
101+
public function it_is_not_authorized_without_permission()
102+
{
103+
$this->setTestRoles(['editor' => ['edit users']]);
104+
105+
$impersonator = tap(User::make()->email('admin@example.com')->assignRole('editor'))->save();
106+
$impersonated = tap(User::make()->email('user@example.com'))->save();
107+
108+
$this->assertFalse((new Action)->authorize($impersonator, $impersonated));
109+
}
110+
111+
#[Test]
112+
public function super_users_bypass_the_policy_check()
113+
{
114+
$impersonator = tap(User::make()->email('admin@example.com')->makeSuper())->save();
115+
$impersonated = tap(User::make()->email('user@example.com'))->save();
116+
117+
Gate::policy(get_class($impersonated), DenyImpersonationPolicy::class);
118+
119+
$this->actingAs($impersonator);
120+
121+
$this->assertTrue((new Action)->visibleTo($impersonated));
122+
$this->assertTrue((new Action)->authorize($impersonator, $impersonated));
123+
}
124+
}
125+
126+
class DenyImpersonationPolicy extends UserPolicy
127+
{
128+
public function impersonate($authed, $user)
129+
{
130+
return false;
131+
}
44132
}

0 commit comments

Comments
 (0)