From f79c1b9efe407bf5048ec800de3b615fdcefca63 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 24 Aug 2026 10:14:32 +0100 Subject: [PATCH 1/3] skip two factor setup enforcement while impersonating Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01WfgWVorn6h1mPSwgGKVb7Q --- .../RedirectIfTwoFactorSetupIncomplete.php | 1 + tests/Feature/Users/TwoFactorRoutesTest.php | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/Http/Middleware/RedirectIfTwoFactorSetupIncomplete.php b/src/Http/Middleware/RedirectIfTwoFactorSetupIncomplete.php index bb223a7e0b5..0ef07f1b07f 100644 --- a/src/Http/Middleware/RedirectIfTwoFactorSetupIncomplete.php +++ b/src/Http/Middleware/RedirectIfTwoFactorSetupIncomplete.php @@ -17,6 +17,7 @@ public function handle(Request $request, Closure $next) && ($user = User::fromUser($request->user())) && $user->isTwoFactorAuthenticationRequired() && ! $user->hasEnabledTwoFactorAuthentication() + && ! session()->has('statamic_impersonated_by') && ! $this->isSetupUrl($request) ) { if (empty($user->two_factor_secret)) { diff --git a/tests/Feature/Users/TwoFactorRoutesTest.php b/tests/Feature/Users/TwoFactorRoutesTest.php index 08dbb7b8b32..963215aac35 100644 --- a/tests/Feature/Users/TwoFactorRoutesTest.php +++ b/tests/Feature/Users/TwoFactorRoutesTest.php @@ -67,6 +67,24 @@ public function cp_two_factor_setup_middleware_redirects_when_two_factor_is_enfo ->assertSessionHas('url.intended', cp_route('dashboard')); } + #[Test] + public function cp_two_factor_setup_middleware_does_not_redirect_when_impersonating() + { + config()->set('statamic.users.two_factor_enforced_roles', ['*']); + + $impersonator = tap(User::make()->makeSuper()->email('impersonator@domain.com'))->save(); + $user = tap(User::make()->makeSuper()->email('admin@domain.com'))->save(); + + $this + ->actingAs($user) + ->withSession(['statamic_impersonated_by' => $impersonator->id()]) + ->get(cp_route('dashboard')) + ->assertOk(); + + $this->assertNull($user->fresh()->two_factor_secret); + $this->assertNull($user->fresh()->two_factor_recovery_codes); + } + #[Test] #[DefineEnvironment('disableTwoFactor')] public function cp_two_factor_setup_middleware_does_not_redirect_when_two_factor_is_disabled() @@ -148,6 +166,24 @@ public function frontend_two_factor_setup_middleware_does_not_regenerate_existin $this->assertEquals($existing, $user->fresh()->two_factor_secret); } + #[Test] + public function frontend_two_factor_setup_middleware_does_not_redirect_when_impersonating() + { + config()->set('statamic.users.two_factor_enforced_roles', ['*']); + + $impersonator = tap(User::make()->makeSuper()->email('impersonator@domain.com'))->save(); + $user = tap(User::make()->makeSuper()->email('admin@domain.com'))->save(); + + $this + ->actingAs($user) + ->withSession(['statamic_impersonated_by' => $impersonator->id()]) + ->get('/test-frontend-route') + ->assertOk(); + + $this->assertNull($user->fresh()->two_factor_secret); + $this->assertNull($user->fresh()->two_factor_recovery_codes); + } + #[Test] public function frontend_two_factor_setup_middleware_redirects_to_configured_url() { From db68c163a0f0fd954ee541b8f5cb6a9219b0c0b0 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 24 Aug 2026 10:19:33 +0100 Subject: [PATCH 2/3] import `Log` facade to fix phpstan analysis Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019fa8J46Th4Q3aRPWs51kPt --- src/Auth/Protect/Protection.php | 3 ++- src/Stache/Stache.php | 7 ++++--- src/Tags/Assets.php | 3 ++- src/Tags/Glide.php | 7 ++++--- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Auth/Protect/Protection.php b/src/Auth/Protect/Protection.php index 75031ca3a7c..3499f354557 100644 --- a/src/Auth/Protect/Protection.php +++ b/src/Auth/Protect/Protection.php @@ -2,6 +2,7 @@ namespace Statamic\Auth\Protect; +use Illuminate\Support\Facades\Log; use InvalidArgumentException; use Statamic\Contracts\Auth\Protect\Protectable; use Statamic\Facades\URL; @@ -82,7 +83,7 @@ protected function url() protected function log($message) { - \Log::debug(vsprintf('%s Denying access to %s.', [ + Log::debug(vsprintf('%s Denying access to %s.', [ $message, $this->url(), ])); diff --git a/src/Stache/Stache.php b/src/Stache/Stache.php index 6b0dc622a77..1b25b25591c 100644 --- a/src/Stache/Stache.php +++ b/src/Stache/Stache.php @@ -6,6 +6,7 @@ use Illuminate\Support\Collection; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Concurrency; +use Illuminate\Support\Facades\Log; use Statamic\Events\StacheCleared; use Statamic\Events\StacheWarmed; use Statamic\Extensions\FileStore; @@ -271,7 +272,7 @@ protected function shouldUseParallelWarming($stores): bool // Disable parallel processing if using Redis cache (serialization issues) $cacheDriver = config('statamic.stache.cache_store', config('cache.default')); if ($cacheDriver === 'redis') { - \Log::info('Parallel warming disabled due to Redis cache driver'); + Log::info('Parallel warming disabled due to Redis cache driver'); return false; } @@ -303,12 +304,12 @@ protected function warmInParallel($stores) $driver = $config['concurrency_driver'] ?? 'process'; if (empty($closures)) { - \Log::info('Closures are empty, skipping parallel warming'); + Log::info('Closures are empty, skipping parallel warming'); } Concurrency::driver($driver)->run($closures); } catch (\Exception $e) { - \Log::warning('Parallel warming failed, falling back to sequential: '.$e->getMessage()); + Log::warning('Parallel warming failed, falling back to sequential: '.$e->getMessage()); $stores->each->warm(); } } diff --git a/src/Tags/Assets.php b/src/Tags/Assets.php index e3c1d818f07..2dc4b12fc0e 100644 --- a/src/Tags/Assets.php +++ b/src/Tags/Assets.php @@ -2,6 +2,7 @@ namespace Statamic\Tags; +use Illuminate\Support\Facades\Log; use Statamic\Assets\Asset as AssetModel; use Statamic\Assets\AssetCollection; use Statamic\Contracts\Query\Builder; @@ -88,7 +89,7 @@ public function index() protected function assetsFromContainer($id, $path) { if (! $id && ! $path) { - \Log::debug('No asset container ID or path was specified.'); + Log::debug('No asset container ID or path was specified.'); return collect(); } diff --git a/src/Tags/Glide.php b/src/Tags/Glide.php index 91b51318cfd..8b3bcd861d8 100644 --- a/src/Tags/Glide.php +++ b/src/Tags/Glide.php @@ -4,6 +4,7 @@ use Facades\Statamic\Imaging\Attributes; use Facades\Statamic\Imaging\ImageValidator; +use Illuminate\Support\Facades\Log; use League\Glide\Server; use Statamic\Contracts\Assets\Asset as AssetContract; use Statamic\Contracts\Data\Augmentable; @@ -146,7 +147,7 @@ public function generate($items = null) return $data; } catch (\Exception $e) { - \Log::error($e->getMessage()); + Log::error($e->getMessage()); } })->filter()->all(); @@ -205,7 +206,7 @@ private function generateGlideUrl($item) try { $url = $this->isValidExtension($item) ? $this->getManipulator($item)->build() : $this->normalizeItem($item); } catch (\Exception $e) { - \Log::error($e->getMessage()); + Log::error($e->getMessage()); return; } @@ -230,7 +231,7 @@ private function generateGlideDataUrl($item) $source = $cache->read($path); $url = 'data:'.$cache->mimeType($path).';base64,'.base64_encode($source); } catch (\Exception $e) { - \Log::error($e->getMessage()); + Log::error($e->getMessage()); return; } From ff28ec6d898f1f6deda77def08f7d8f29191ab98 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 24 Aug 2026 10:35:07 +0100 Subject: [PATCH 3/3] revert phpstan fix, split into separate pr Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019fa8J46Th4Q3aRPWs51kPt --- src/Auth/Protect/Protection.php | 3 +-- src/Stache/Stache.php | 7 +++---- src/Tags/Assets.php | 3 +-- src/Tags/Glide.php | 7 +++---- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/Auth/Protect/Protection.php b/src/Auth/Protect/Protection.php index 3499f354557..75031ca3a7c 100644 --- a/src/Auth/Protect/Protection.php +++ b/src/Auth/Protect/Protection.php @@ -2,7 +2,6 @@ namespace Statamic\Auth\Protect; -use Illuminate\Support\Facades\Log; use InvalidArgumentException; use Statamic\Contracts\Auth\Protect\Protectable; use Statamic\Facades\URL; @@ -83,7 +82,7 @@ protected function url() protected function log($message) { - Log::debug(vsprintf('%s Denying access to %s.', [ + \Log::debug(vsprintf('%s Denying access to %s.', [ $message, $this->url(), ])); diff --git a/src/Stache/Stache.php b/src/Stache/Stache.php index 1b25b25591c..6b0dc622a77 100644 --- a/src/Stache/Stache.php +++ b/src/Stache/Stache.php @@ -6,7 +6,6 @@ use Illuminate\Support\Collection; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Concurrency; -use Illuminate\Support\Facades\Log; use Statamic\Events\StacheCleared; use Statamic\Events\StacheWarmed; use Statamic\Extensions\FileStore; @@ -272,7 +271,7 @@ protected function shouldUseParallelWarming($stores): bool // Disable parallel processing if using Redis cache (serialization issues) $cacheDriver = config('statamic.stache.cache_store', config('cache.default')); if ($cacheDriver === 'redis') { - Log::info('Parallel warming disabled due to Redis cache driver'); + \Log::info('Parallel warming disabled due to Redis cache driver'); return false; } @@ -304,12 +303,12 @@ protected function warmInParallel($stores) $driver = $config['concurrency_driver'] ?? 'process'; if (empty($closures)) { - Log::info('Closures are empty, skipping parallel warming'); + \Log::info('Closures are empty, skipping parallel warming'); } Concurrency::driver($driver)->run($closures); } catch (\Exception $e) { - Log::warning('Parallel warming failed, falling back to sequential: '.$e->getMessage()); + \Log::warning('Parallel warming failed, falling back to sequential: '.$e->getMessage()); $stores->each->warm(); } } diff --git a/src/Tags/Assets.php b/src/Tags/Assets.php index 2dc4b12fc0e..e3c1d818f07 100644 --- a/src/Tags/Assets.php +++ b/src/Tags/Assets.php @@ -2,7 +2,6 @@ namespace Statamic\Tags; -use Illuminate\Support\Facades\Log; use Statamic\Assets\Asset as AssetModel; use Statamic\Assets\AssetCollection; use Statamic\Contracts\Query\Builder; @@ -89,7 +88,7 @@ public function index() protected function assetsFromContainer($id, $path) { if (! $id && ! $path) { - Log::debug('No asset container ID or path was specified.'); + \Log::debug('No asset container ID or path was specified.'); return collect(); } diff --git a/src/Tags/Glide.php b/src/Tags/Glide.php index 8b3bcd861d8..91b51318cfd 100644 --- a/src/Tags/Glide.php +++ b/src/Tags/Glide.php @@ -4,7 +4,6 @@ use Facades\Statamic\Imaging\Attributes; use Facades\Statamic\Imaging\ImageValidator; -use Illuminate\Support\Facades\Log; use League\Glide\Server; use Statamic\Contracts\Assets\Asset as AssetContract; use Statamic\Contracts\Data\Augmentable; @@ -147,7 +146,7 @@ public function generate($items = null) return $data; } catch (\Exception $e) { - Log::error($e->getMessage()); + \Log::error($e->getMessage()); } })->filter()->all(); @@ -206,7 +205,7 @@ private function generateGlideUrl($item) try { $url = $this->isValidExtension($item) ? $this->getManipulator($item)->build() : $this->normalizeItem($item); } catch (\Exception $e) { - Log::error($e->getMessage()); + \Log::error($e->getMessage()); return; } @@ -231,7 +230,7 @@ private function generateGlideDataUrl($item) $source = $cache->read($path); $url = 'data:'.$cache->mimeType($path).';base64,'.base64_encode($source); } catch (\Exception $e) { - Log::error($e->getMessage()); + \Log::error($e->getMessage()); return; }