Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion app/Http/Controllers/ItemController.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ public function dash(Request $request): View
$data["treat_tags_as"] = $treat_tags_as;

if (config('app.auth_roles_enable')) {
$roles = explode(config('app.auth_roles_delimiter'), $request->header(config('app.auth_roles_header')));
// Coalesce before exploding: the header is absent on any request
// that did not come through the proxy, and passing null to
// explode() is deprecated in PHP 8.1 and removed later.
$roles = explode(config('app.auth_roles_delimiter'), $request->header(config('app.auth_roles_header')) ?? '');
if ($treat_tags_as == 'categories') {
$data['categories'] = Item::whereHas('children')->with('children', function ($query) {
$query->pinned()->orderBy('order', 'asc');
Expand Down
12 changes: 11 additions & 1 deletion app/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,17 @@ public function boot(): void
$view->with('allusers', $allusers);
$view->with('current_user', $current_user);
if (config('app.auth_roles_enable')) {
$view->with('enable_auth_admin_controls', in_array(config('app.auth_roles_admin'), explode(config('app.auth_roles_delimiter'), $_SERVER[config('app.auth_roles_http_header')])));
// Anything that reaches Heimdall without passing through the
// proxy arrives with no roles header: a container healthcheck,
// a probe on the published port, a proxy that is not set up
// yet. Reading the key unguarded turns each of those into a
// 500 for the whole view. Treat it as holding no roles, which
// fails closed on the admin check below.
$roles = $_SERVER[config('app.auth_roles_http_header')] ?? '';
$view->with(
'enable_auth_admin_controls',
in_array(config('app.auth_roles_admin'), explode(config('app.auth_roles_delimiter'), $roles))
);
} else {
$view->with('enable_auth_admin_controls', true);
}
Expand Down
95 changes: 95 additions & 0 deletions tests/Feature/RolesHeaderMissingTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

namespace Tests\Feature;

use App\Item;
use App\ItemTag;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

/**
* Roles mode with the roles header absent.
*
* The header is supplied by the reverse proxy, so it is missing whenever
* something reaches Heimdall without going through it: a healthcheck on the
* container port, a probe on the published port, a proxy that has not been
* configured yet, or a request the proxy passes through unauthenticated.
* None of those should take the dashboard down.
*/
class RolesHeaderMissingTest extends TestCase
{
use RefreshDatabase;

private const ROLES_HEADER = 'X-Roles';

private const ROLES_SERVER_KEY = 'HTTP_X_ROLES';

protected function setUp(): void
{
parent::setUp();

config([
'app.auth_roles_enable' => true,
'app.auth_roles_header' => self::ROLES_HEADER,
'app.auth_roles_http_header' => self::ROLES_SERVER_KEY,
'app.auth_roles_admin' => 'admin',
'app.auth_roles_delimiter' => ',',
]);

// Nothing set the header: this is the state the bug is about.
unset($_SERVER[self::ROLES_SERVER_KEY]);
}

public function test_dash_still_renders_when_the_roles_header_is_absent(): void
{
$this->seed();

$response = $this->get('/');

$response->assertStatus(200);
}

public function test_a_request_without_the_roles_header_gets_no_admin_controls(): void
{
$this->seed();

$response = $this->get('/');

$response->assertStatus(200);
// Degrading has to fail closed. An absent header is no roles at all,
// never the admin role.
$response->assertDontSee('id="config-buttons"', false);
}

public function test_a_request_without_the_roles_header_gets_no_dashboard_tiles(): void
{
$this->seed();

$item = Item::factory()->create([
'title' => 'Unifi Controller',
'role' => 'admin',
'pinned' => 1,
]);
ItemTag::factory()->create(['item_id' => $item->id, 'tag_id' => 0]);

$response = $this->get('/');

$response->assertStatus(200);
// No roles means no tiles, rather than an error page or every tile.
$response->assertDontSee('data-name="Unifi Controller"', false);
}

public function test_the_admin_role_is_still_honoured_when_the_header_is_present(): void
{
$this->seed();

$_SERVER[self::ROLES_SERVER_KEY] = 'admin';

$response = $this->get('/', [self::ROLES_HEADER => 'admin']);

$response->assertStatus(200);
$response->assertSee('id="config-buttons"', false);

unset($_SERVER[self::ROLES_SERVER_KEY]);
}
}