Skip to content

Commit

Permalink
[5.x] Add entry password protection (#10800)
Browse files Browse the repository at this point in the history
Co-authored-by: Jason Varga <[email protected]>
  • Loading branch information
aerni and jasonvarga authored Sep 17, 2024
1 parent fcc4461 commit 9722f73
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 37 deletions.
1 change: 1 addition & 0 deletions config/protect.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
'password' => [
'driver' => 'password',
'allowed' => ['secret'],
'field' => null,
'form_url' => null,
],

Expand Down
27 changes: 20 additions & 7 deletions src/Auth/Protect/Protectors/Password/Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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');
}

Expand All @@ -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'];
Expand All @@ -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;
}
Expand Down
22 changes: 0 additions & 22 deletions src/Auth/Protect/Protectors/Password/Guard.php

This file was deleted.

50 changes: 46 additions & 4 deletions src/Auth/Protect/Protectors/Password/PasswordProtector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand All @@ -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()
Expand All @@ -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;
Expand Down
134 changes: 131 additions & 3 deletions tests/Auth/Protect/PasswordEntryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand All @@ -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',
Expand All @@ -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
Expand All @@ -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',
]);
}
}
3 changes: 2 additions & 1 deletion tests/Auth/Protect/PasswordProtectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
]);
}

Expand Down Expand Up @@ -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')
Expand Down

0 comments on commit 9722f73

Please sign in to comment.