diff --git a/src/Actions/Impersonate.php b/src/Actions/Impersonate.php index 37c58f0aff8..0f6368b6f73 100644 --- a/src/Actions/Impersonate.php +++ b/src/Actions/Impersonate.php @@ -24,7 +24,11 @@ public function visibleTo($item) return false; } - return $item instanceof UserContract && $item->id() != User::current()->id(); + if (! ($item instanceof UserContract && $item->id() != User::current()->id())) { + return false; + } + + return User::current()->can('impersonate', $item); } public function visibleToBulk($items) @@ -34,7 +38,7 @@ public function visibleToBulk($items) public function authorize($authed, $user) { - return $authed->can('impersonate users'); + return $authed->can('impersonate', $user); } public function run($users, $values) diff --git a/src/Policies/UserPolicy.php b/src/Policies/UserPolicy.php index c691d2256ba..5c62d60a642 100644 --- a/src/Policies/UserPolicy.php +++ b/src/Policies/UserPolicy.php @@ -67,4 +67,11 @@ public function sendPasswordReset($authed, $user) { return $this->edit($authed, $user); } + + public function impersonate($authed, $user) + { + $authed = User::fromUser($authed); + + return $authed->hasPermission('impersonate users'); + } } diff --git a/tests/Actions/ImpersonateTest.php b/tests/Actions/ImpersonateTest.php index 658f6ec6c95..84fa8ddae6c 100644 --- a/tests/Actions/ImpersonateTest.php +++ b/tests/Actions/ImpersonateTest.php @@ -2,10 +2,14 @@ namespace Tests\Actions; +use Illuminate\Support\Facades\Gate; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; +use Statamic\Actions\Impersonate as Action; use Statamic\Facades\User; +use Statamic\Policies\UserPolicy; use Tests\ElevatesSessions; +use Tests\FakesRoles; use Tests\PreventSavingStacheItemsToDisk; use Tests\TestCase; @@ -13,6 +17,7 @@ class ImpersonateTest extends TestCase { use ElevatesSessions; + use FakesRoles; use PreventSavingStacheItemsToDisk; private function impersonate($user) @@ -41,4 +46,87 @@ public function it_authenticates_as_another_user_and_clears_elevated_session() $this->assertEquals($impersonated->id(), auth()->id()); $this->assertFalse(request()->hasElevatedSession()); } + + #[Test] + public function it_is_visible_to_a_valid_target_user() + { + $impersonator = tap(User::make()->email('admin@example.com')->makeSuper())->save(); + $impersonated = tap(User::make()->email('user@example.com'))->save(); + + $this->actingAs($impersonator); + + $this->assertTrue((new Action)->visibleTo($impersonated)); + } + + #[Test] + public function it_is_not_visible_when_policy_denies_impersonation() + { + $this->setTestRoles(['impersonator' => ['impersonate users']]); + + $impersonator = tap(User::make()->email('admin@example.com')->assignRole('impersonator'))->save(); + $impersonated = tap(User::make()->email('user@example.com'))->save(); + + Gate::policy(get_class($impersonated), DenyImpersonationPolicy::class); + + $this->actingAs($impersonator); + + $this->assertFalse((new Action)->visibleTo($impersonated)); + } + + #[Test] + public function it_is_authorized_with_the_default_policy() + { + $this->setTestRoles(['impersonator' => ['impersonate users']]); + + $impersonator = tap(User::make()->email('admin@example.com')->assignRole('impersonator'))->save(); + $impersonated = tap(User::make()->email('user@example.com'))->save(); + + $this->assertTrue((new Action)->authorize($impersonator, $impersonated)); + } + + #[Test] + public function it_is_not_authorized_when_policy_denies_impersonation() + { + $this->setTestRoles(['impersonator' => ['impersonate users']]); + + $impersonator = tap(User::make()->email('admin@example.com')->assignRole('impersonator'))->save(); + $impersonated = tap(User::make()->email('user@example.com'))->save(); + + Gate::policy(get_class($impersonated), DenyImpersonationPolicy::class); + + $this->assertFalse((new Action)->authorize($impersonator, $impersonated)); + } + + #[Test] + public function it_is_not_authorized_without_permission() + { + $this->setTestRoles(['editor' => ['edit users']]); + + $impersonator = tap(User::make()->email('admin@example.com')->assignRole('editor'))->save(); + $impersonated = tap(User::make()->email('user@example.com'))->save(); + + $this->assertFalse((new Action)->authorize($impersonator, $impersonated)); + } + + #[Test] + public function super_users_bypass_the_policy_check() + { + $impersonator = tap(User::make()->email('admin@example.com')->makeSuper())->save(); + $impersonated = tap(User::make()->email('user@example.com'))->save(); + + Gate::policy(get_class($impersonated), DenyImpersonationPolicy::class); + + $this->actingAs($impersonator); + + $this->assertTrue((new Action)->visibleTo($impersonated)); + $this->assertTrue((new Action)->authorize($impersonator, $impersonated)); + } +} + +class DenyImpersonationPolicy extends UserPolicy +{ + public function impersonate($authed, $user) + { + return false; + } }