Skip to content

Commit 13684be

Browse files
author
tchapi
committed
chore
1 parent cb29fd2 commit 13684be

7 files changed

Lines changed: 69 additions & 2 deletions

File tree

config/packages/security.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ security:
2121
logout:
2222
path: app_logout
2323
target: dashboard
24+
# Without this, any page the admin visits can log them out with a plain GET
25+
enable_csrf: true
2426

2527

2628
access_control:

templates/_partials/navigation.html.twig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
👤 {{ app.user.username }}
2323
</a>
2424
<div class="dropdown-menu" aria-labelledby="navUserMenu">
25-
<a class="dropdown-item" href="{{ path('app_logout') }}">{{ "logout"|trans }}</a>
25+
<a class="dropdown-item" href="{{ path('app_logout', {_csrf_token: csrf_token('logout')}) }}">{{ "logout"|trans }}</a>
2626
</div>
2727
</li>
2828
<li class="nav-item dropdown">

templates/security/login.html.twig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
{% if app.user %}
77
<div class="mb-3">
8-
{{ "login.already"|trans({username: app.user.username}) }}, <a href="{{ path('app_logout') }}">{{ "logout"|trans }}</a>
8+
{{ "login.already"|trans({username: app.user.username}) }}, <a href="{{ path('app_logout', {_csrf_token: csrf_token('logout')}) }}">{{ "logout"|trans }}</a>
99
</div>
1010
{% else %}
1111
<div class="row justify-content-md-center">

tests/Functional/Controllers/AddressBookControllerTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,4 +206,15 @@ public function testAddressBookWithoutADisplayNameFallsBackToItsUri(): void
206206
$this->assertResponseIsSuccessful();
207207
$this->assertAnySelectorTextContains('h5', 'nameless-book');
208208
}
209+
210+
public function testAddressBookPagesAreNotReachableAnonymously(): void
211+
{
212+
$client = static::createClient();
213+
214+
foreach (['/addressbooks/1', '/addressbooks/1/new', '/addressbooks/1/edit/1'] as $url) {
215+
$client->request('GET', $url);
216+
217+
$this->assertResponseRedirects('/login', null, $url.' must not be public');
218+
}
219+
}
209220
}

tests/Functional/Controllers/CalendarControllerTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,4 +313,15 @@ public function testCalendarNewIgnoresASubmittedOwner(): void
313313
$calendarRepository = static::getContainer()->get(CalendarInstanceRepository::class);
314314
$this->assertNull($calendarRepository->findOneBy(['uri' => 'hijack']));
315315
}
316+
317+
public function testCalendarPagesAreNotReachableAnonymously(): void
318+
{
319+
$client = static::createClient();
320+
321+
foreach (['/calendars/1', '/calendars/1/new', '/calendars/1/edit/1', '/calendars/1/shares/1'] as $url) {
322+
$client->request('GET', $url);
323+
324+
$this->assertResponseRedirects('/login', null, $url.' must not be public');
325+
}
326+
}
316327
}

tests/Functional/Controllers/DashboardTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace App\Tests\Functional;
44

5+
use App\Security\AdminUser;
56
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
67

78
class DashboardTest extends WebTestCase
@@ -91,4 +92,31 @@ public function testLoginCorrect(): void
9192
$this->assertSelectorTextContains('h3.environment', 'Configured environment');
9293
$this->assertSelectorExists('nav.navbar');
9394
}
95+
96+
/**
97+
* A plain `GET /logout` from any page the admin happens to visit used to end their session.
98+
*/
99+
public function testLogoutRequiresACsrfToken(): void
100+
{
101+
$client = static::createClient();
102+
$client->loginUser(new AdminUser('admin', 'test'));
103+
104+
$client->request('GET', '/logout');
105+
$this->assertResponseStatusCodeSame(403);
106+
107+
$client->request('GET', '/dashboard');
108+
$this->assertResponseIsSuccessful('The session must survive a logout without a token');
109+
}
110+
111+
public function testLogoutWorksFromTheMenuLink(): void
112+
{
113+
$client = static::createClient();
114+
$client->loginUser(new AdminUser('admin', 'test'));
115+
116+
$crawler = $client->request('GET', '/dashboard');
117+
$client->click($crawler->filter('a.dropdown-item')->selectLink('Logout')->link());
118+
119+
$client->request('GET', '/dashboard');
120+
$this->assertResponseRedirects('/login');
121+
}
94122
}

tests/Functional/Controllers/UserControllerTest.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,4 +253,19 @@ public function testUserCreationRejectsAUsernameThatBreaksThePrincipalUri(): voi
253253
static::getContainer()->get('doctrine.orm.entity_manager')->getRepository(User::class)->findOneByUsername('bad/user')
254254
);
255255
}
256+
257+
/**
258+
* Every admin test authenticates first, so a missing `access_control` entry would go
259+
* unnoticed — which is exactly how the `^/adressbooks` typo of #268 shipped.
260+
*/
261+
public function testUserPagesAreNotReachableAnonymously(): void
262+
{
263+
$client = static::createClient();
264+
265+
foreach (['/users/', '/users/new', '/users/edit/1', '/users/delegates/1'] as $url) {
266+
$client->request('GET', $url);
267+
268+
$this->assertResponseRedirects('/login', null, $url.' must not be public');
269+
}
270+
}
256271
}

0 commit comments

Comments
 (0)