diff --git a/config/protect.php b/config/protect.php index 383a2d02cf..4e78f78a7f 100644 --- a/config/protect.php +++ b/config/protect.php @@ -44,6 +44,7 @@ 'password' => [ 'driver' => 'password', 'allowed' => ['secret'], + 'field' => null, 'form_url' => null, ], diff --git a/src/Auth/Protect/Protectors/Password/Controller.php b/src/Auth/Protect/Protectors/Password/Controller.php index 072afc7a5a..dcc0d5fdf0 100644 --- a/src/Auth/Protect/Protectors/Password/Controller.php +++ b/src/Auth/Protect/Protectors/Password/Controller.php @@ -2,6 +2,8 @@ namespace Statamic\Auth\Protect\Protectors\Password; +use Statamic\Auth\Protect\ProtectorManager; +use Statamic\Facades\Data; use Statamic\Facades\Site; use Statamic\Http\Controllers\Controller as BaseController; use Statamic\View\View; @@ -31,9 +33,7 @@ public function store() return back()->withErrors(['token' => __('statamic::messages.password_protect_token_invalid')], 'passwordProtect'); } - $guard = new Guard($this->getScheme()); - - if (! $guard->check($this->password)) { + if (! $this->driver()->isValidPassword($this->password)) { return back()->withErrors(['password' => __('statamic::messages.password_protect_incorrect_password')], 'passwordProtect'); } @@ -43,6 +43,13 @@ public function store() ->redirect(); } + private function driver(): PasswordProtector + { + return app(ProtectorManager::class) + ->driver($this->getScheme()) + ->setData(Data::find($this->getReference())); + } + protected function getScheme() { return $this->tokenData['scheme']; @@ -53,12 +60,18 @@ protected function getUrl() return $this->tokenData['url']; } + protected function getReference() + { + return $this->tokenData['reference']; + } + protected function storePassword() { - session()->put( - "statamic:protect:password.passwords.{$this->getScheme()}", - $this->password - ); + $sessionKey = $this->driver()->isValidLocalPassword($this->password) + ? "statamic:protect:password.passwords.ref.{$this->getReference()}" + : "statamic:protect:password.passwords.scheme.{$this->getScheme()}"; + + session()->put($sessionKey, $this->password); return $this; } diff --git a/src/Auth/Protect/Protectors/Password/Guard.php b/src/Auth/Protect/Protectors/Password/Guard.php deleted file mode 100644 index 3ec5f0f168..0000000000 --- a/src/Auth/Protect/Protectors/Password/Guard.php +++ /dev/null @@ -1,22 +0,0 @@ -config = config("statamic.protect.schemes.$scheme"); - } - - public function check($password) - { - $allowed = Arr::get($this->config, 'allowed', []); - - return in_array($password, $allowed); - } -} diff --git a/src/Auth/Protect/Protectors/Password/PasswordProtector.php b/src/Auth/Protect/Protectors/Password/PasswordProtector.php index d7af6a5722..bafab41153 100644 --- a/src/Auth/Protect/Protectors/Password/PasswordProtector.php +++ b/src/Auth/Protect/Protectors/Password/PasswordProtector.php @@ -16,7 +16,7 @@ class PasswordProtector extends Protector */ public function protect() { - if (empty(Arr::get($this->config, 'allowed', []))) { + if (empty($this->schemePasswords()) && ! $this->localPasswords()) { throw new ForbiddenHttpException(); } @@ -33,11 +33,52 @@ public function protect() } } + protected function schemePasswords() + { + return Arr::get($this->config, 'allowed', []); + } + + public function localPasswords() + { + if (! $field = Arr::get($this->config, 'field')) { + return []; + } + + return Arr::wrap($this->data->$field); + } + public function hasEnteredValidPassword() { - return (new Guard($this->scheme))->check( - session("statamic:protect:password.passwords.{$this->scheme}") - ); + if ( + ($password = session("statamic:protect:password.passwords.scheme.{$this->scheme}")) + && $this->isValidSchemePassword($password) + ) { + return true; + } + + if ( + ($password = session("statamic:protect:password.passwords.ref.{$this->data->reference()}")) + && $this->isValidLocalPassword($password) + ) { + return true; + } + + return false; + } + + public function isValidPassword(string $password): bool + { + return $this->isValidSchemePassword($password) || $this->isValidLocalPassword($password); + } + + public function isValidSchemePassword(string $password): bool + { + return in_array($password, $this->schemePasswords()); + } + + public function isValidLocalPassword(string $password): bool + { + return in_array($password, $this->localPasswords()); } protected function isPasswordFormUrl() @@ -64,6 +105,7 @@ protected function generateToken() session()->put("statamic:protect:password.tokens.$token", [ 'scheme' => $this->scheme, 'url' => $this->url, + 'reference' => $this->data->reference(), ]); return $token; diff --git a/tests/Auth/Protect/PasswordEntryTest.php b/tests/Auth/Protect/PasswordEntryTest.php index 07bf031e8e..cd8a89e1dd 100644 --- a/tests/Auth/Protect/PasswordEntryTest.php +++ b/tests/Auth/Protect/PasswordEntryTest.php @@ -2,10 +2,11 @@ namespace Tests\Auth\Protect; +use Facades\Statamic\Auth\Protect\Protectors\Password\Token; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; -use Tests\TestCase; -class PasswordEntryTest extends TestCase +class PasswordEntryTest extends PageProtectionTestCase { #[Test] public function it_returns_back_with_error_if_theres_no_token() @@ -31,6 +32,7 @@ public function it_returns_back_with_error_if_the_wrong_password_is_entered() session()->put('statamic:protect:password.tokens.test-token', [ 'scheme' => 'password-scheme', 'url' => '/target-url', + 'reference' => 'entry::test', ]); $this @@ -47,6 +49,7 @@ public function it_returns_back_with_error_if_the_wrong_password_is_entered() public function it_allows_access_if_allowed_password_was_entered() { $this->withoutExceptionHandling(); + config(['statamic.protect.schemes.password-scheme' => [ 'driver' => 'password', 'form_url' => '/password-entry', @@ -56,6 +59,7 @@ public function it_allows_access_if_allowed_password_was_entered() session()->put('statamic:protect:password.tokens.test-token', [ 'scheme' => 'password-scheme', 'url' => '/target-url', + 'reference' => 'entry::test', ]); $this @@ -64,7 +68,131 @@ public function it_allows_access_if_allowed_password_was_entered() 'password' => 'the-password', ]) ->assertRedirect('http://localhost/target-url') - ->assertSessionHas('statamic:protect:password.passwords.password-scheme', 'the-password') + ->assertSessionHas('statamic:protect:password.passwords.scheme.password-scheme', 'the-password') + ->assertSessionMissing('statamic:protect:password.tokens.test-token'); + } + + #[Test] + #[DataProvider('localPasswordProvider')] + public function it_allows_access_if_local_password_was_entered( + $passwordFieldInContent, + $submittedPassword, + ) { + config(['statamic.protect.schemes.password-scheme' => [ + 'driver' => 'password', + 'allowed' => ['the-scheme-password'], + 'field' => 'password', + ]]); + + Token::shouldReceive('generate')->andReturn('test-token'); + + $this->createPage('test', ['data' => ['protect' => 'password-scheme', 'password' => $passwordFieldInContent]]); + + $this->get('test') + ->assertSessionHas('statamic:protect:password.tokens.test-token', [ + 'scheme' => 'password-scheme', + 'url' => 'http://localhost/test', + 'reference' => 'entry::test', + ]); + + $this + ->post('/!/protect/password', [ + 'token' => 'test-token', + 'password' => $submittedPassword, + ]) + ->assertRedirect('http://localhost/test') + ->assertSessionHas('statamic:protect:password.passwords.ref.entry::test', $submittedPassword) + ->assertSessionMissing('statamic:protect:password.passwords.password-scheme') + ->assertSessionMissing('statamic:protect:password.tokens.test-token'); + } + + public static function localPasswordProvider() + { + return [ + 'string' => [ + 'value' => 'the-local-password', + 'submitted' => 'the-local-password', + ], + 'array with single value' => [ + 'value' => ['the-local-password'], + 'submitted' => 'the-local-password', + ], + 'array with multiple values' => [ + 'value' => ['first-local-password', 'second-local-password'], + 'submitted' => 'second-local-password', + ], + ]; + } + + #[Test] + public function it_prefers_the_local_password_over_the_scheme_password() + { + config(['statamic.protect.schemes.password-scheme' => [ + 'driver' => 'password', + 'allowed' => ['the-scheme-password'], + 'field' => 'password', + ]]); + + Token::shouldReceive('generate')->andReturn('test-token'); + + $this->createPage('test', ['data' => ['protect' => 'password-scheme', 'password' => 'the-scheme-password']]); + + $this->get('test') + ->assertSessionHas('statamic:protect:password.tokens.test-token', [ + 'scheme' => 'password-scheme', + 'url' => 'http://localhost/test', + 'reference' => 'entry::test', + ]); + + $this + ->post('/!/protect/password', [ + 'token' => 'test-token', + 'password' => 'the-scheme-password', + ]) + ->assertRedirect('http://localhost/test') + ->assertSessionHas('statamic:protect:password.passwords.ref.entry::test', 'the-scheme-password') + ->assertSessionMissing('statamic:protect:password.passwords.password-scheme') ->assertSessionMissing('statamic:protect:password.tokens.test-token'); } + + #[Test] + public function it_can_use_the_same_local_password_multiple_times() + { + config(['statamic.protect.schemes.password-scheme' => [ + 'driver' => 'password', + 'allowed' => ['the-scheme-password'], + 'field' => 'password', + ]]); + + Token::shouldReceive('generate')->andReturn('test-token'); + + $this->createPage('test', ['data' => ['protect' => 'password-scheme', 'password' => 'the-local-password']]); + $this->createPage('test-2', ['data' => ['protect' => 'password-scheme', 'password' => 'the-local-password']]); + + $this->get('test') + ->assertRedirect('http://localhost/!/protect/password?token=test-token') + ->assertSessionHas('statamic:protect:password.tokens.test-token', [ + 'scheme' => 'password-scheme', + 'url' => 'http://localhost/test', + 'reference' => 'entry::test', + ]); + + $this + ->post('/!/protect/password', [ + 'token' => 'test-token', + 'password' => 'the-local-password', + ]) + ->assertRedirect('http://localhost/test') + ->assertSessionHas('statamic:protect:password.passwords.ref.entry::test', 'the-local-password'); + + $this->get('test')->assertOk(); + + $this->get('test-2') + ->assertRedirect('http://localhost/!/protect/password?token=test-token') + ->assertSessionHas('statamic:protect:password.tokens.test-token', [ + 'scheme' => 'password-scheme', + 'url' => 'http://localhost/test-2', + 'reference' => 'entry::test-2', + ]); + } } diff --git a/tests/Auth/Protect/PasswordProtectionTest.php b/tests/Auth/Protect/PasswordProtectionTest.php index 4d2258b91b..973896d43c 100644 --- a/tests/Auth/Protect/PasswordProtectionTest.php +++ b/tests/Auth/Protect/PasswordProtectionTest.php @@ -31,6 +31,7 @@ public function redirects_to_password_form_url_and_generates_token() ->assertSessionHas('statamic:protect:password.tokens.test-token', [ 'scheme' => 'password-scheme', 'url' => 'http://localhost/test', + 'reference' => 'entry::test', ]); } @@ -58,7 +59,7 @@ public function allow_access_if_password_has_been_entered_for_that_scheme() 'allowed' => ['the-password'], ]]); - session()->put('statamic:protect:password.passwords.password-scheme', 'the-password'); + session()->put('statamic:protect:password.passwords.scheme.password-scheme', 'the-password'); $this ->requestPageProtectedBy('password-scheme')