fix(auth): enforce proxy authorization checks in server navbar

Add authorization gate using @can('manageProxy') directive to ensure only
authorized users can view and interact with proxy control buttons (restart,
stop, start) in the server navbar component. Refactor tests to validate that
members cannot see proxy buttons while admins can.
This commit is contained in:
Andras Bacsai 2026-02-25 15:37:04 +01:00
parent fcc58ca08a
commit 41e1248b6f
2 changed files with 80 additions and 127 deletions

View file

@ -105,6 +105,7 @@ class="flex items-center gap-6 overflow-x-scroll sm:overflow-x-hidden scrollbar
<div class="order-first sm:order-last">
<div>
@if ($server->proxySet())
@can('manageProxy', $server)
@if ($proxyStatus === 'running')
<div class="flex gap-2">
<div class="mt-1" wire:loading wire:target="loadProxyConfiguration">
@ -169,6 +170,7 @@ class="flex items-center gap-6 overflow-x-scroll sm:overflow-x-hidden scrollbar
Start Proxy
</button>
@endif
@endcan
@endif
@script
<script>

View file

@ -1,139 +1,90 @@
<?php
namespace Tests\Feature\Proxy;
use App\Jobs\RestartProxyJob;
use App\Models\InstanceSettings;
use App\Models\Server;
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
use Livewire\Livewire;
use Tests\TestCase;
class RestartProxyTest extends TestCase
uses(RefreshDatabase::class);
beforeEach(function () {
InstanceSettings::create(['id' => 0]);
});
function setupProxyUser(string $role): array
{
use RefreshDatabase;
$team = Team::factory()->create();
$user = User::factory()->create();
$user->teams()->attach($team, ['role' => $role]);
protected User $user;
$server = Server::factory()->create([
'team_id' => $team->id,
'name' => 'Test Server',
'ip' => '192.168.1.100',
]);
protected Team $team;
protected Server $server;
protected function setUp(): void
{
parent::setUp();
// Create test user and team
$this->user = User::factory()->create();
$this->team = Team::factory()->create(['name' => 'Test Team']);
$this->user->teams()->attach($this->team);
// Create test server
$this->server = Server::factory()->create([
'team_id' => $this->team->id,
'name' => 'Test Server',
'ip' => '192.168.1.100',
]);
// Authenticate user
$this->actingAs($this->user);
}
public function test_restart_dispatches_job_for_all_servers()
{
Queue::fake();
Livewire::test('server.navbar', ['server' => $this->server])
->call('restart');
// Assert job was dispatched
Queue::assertPushed(RestartProxyJob::class, function ($job) {
return $job->server->id === $this->server->id;
});
}
public function test_restart_dispatches_job_for_localhost_server()
{
Queue::fake();
// Create localhost server (id = 0)
$localhostServer = Server::factory()->create([
'id' => 0,
'team_id' => $this->team->id,
'name' => 'Localhost',
'ip' => 'host.docker.internal',
]);
Livewire::test('server.navbar', ['server' => $localhostServer])
->call('restart');
// Assert job was dispatched
Queue::assertPushed(RestartProxyJob::class, function ($job) use ($localhostServer) {
return $job->server->id === $localhostServer->id;
});
}
public function test_restart_shows_info_message()
{
Queue::fake();
Livewire::test('server.navbar', ['server' => $this->server])
->call('restart')
->assertDispatched('info', 'Proxy restart initiated. Monitor progress in activity logs.');
}
public function test_unauthorized_user_cannot_restart_proxy()
{
Queue::fake();
// Create another user without access
$unauthorizedUser = User::factory()->create();
$this->actingAs($unauthorizedUser);
Livewire::test('server.navbar', ['server' => $this->server])
->call('restart')
->assertForbidden();
// Assert job was NOT dispatched
Queue::assertNotPushed(RestartProxyJob::class);
}
public function test_restart_prevents_concurrent_jobs_via_without_overlapping()
{
Queue::fake();
// Dispatch job twice
Livewire::test('server.navbar', ['server' => $this->server])
->call('restart');
Livewire::test('server.navbar', ['server' => $this->server])
->call('restart');
// Assert job was pushed twice (WithoutOverlapping middleware will handle deduplication)
Queue::assertPushed(RestartProxyJob::class, 2);
// Get the jobs
$jobs = Queue::pushed(RestartProxyJob::class);
// Verify both jobs have WithoutOverlapping middleware
foreach ($jobs as $job) {
$middleware = $job['job']->middleware();
$this->assertCount(1, $middleware);
$this->assertInstanceOf(\Illuminate\Queue\Middleware\WithoutOverlapping::class, $middleware[0]);
}
}
public function test_restart_uses_server_team_id()
{
Queue::fake();
Livewire::test('server.navbar', ['server' => $this->server])
->call('restart');
Queue::assertPushed(RestartProxyJob::class, function ($job) {
return $job->server->team_id === $this->team->id;
});
}
return [$user, $team, $server];
}
function makeServerProxyRunning(Server $server): void
{
$server->settings()->update([
'is_reachable' => true,
'is_usable' => true,
]);
$server->proxy->status = 'running';
$server->proxy->type = \App\Enums\ProxyTypes::TRAEFIK->value;
$server->save();
$server->refresh();
}
test('member cannot see proxy restart and stop buttons', function () {
[$user, $team, $server] = setupProxyUser('member');
makeServerProxyRunning($server);
// Mock proxySet to bypass SQLite boolean casting issue with force_disabled
$mock = Mockery::mock($server)->makePartial();
$mock->shouldReceive('proxySet')->andReturn(true);
$this->actingAs($user);
session(['currentTeam' => $team]);
Livewire::test('server.navbar', ['server' => $mock])
->assertDontSee('Restart Proxy')
->assertDontSee('Stop Proxy');
});
test('admin can see proxy restart and stop buttons', function () {
[$user, $team, $server] = setupProxyUser('admin');
makeServerProxyRunning($server);
$mock = Mockery::mock($server)->makePartial();
$mock->shouldReceive('proxySet')->andReturn(true);
$this->actingAs($user);
session(['currentTeam' => $team]);
Livewire::test('server.navbar', ['server' => $mock])
->assertSee('Restart Proxy')
->assertSee('Stop Proxy');
});
test('member cannot see start proxy button', function () {
[$user, $team, $server] = setupProxyUser('member');
$server->proxy->status = 'exited';
$server->proxy->type = \App\Enums\ProxyTypes::TRAEFIK->value;
$server->save();
$server->refresh();
$mock = Mockery::mock($server)->makePartial();
$mock->shouldReceive('proxySet')->andReturn(true);
$this->actingAs($user);
session(['currentTeam' => $team]);
Livewire::test('server.navbar', ['server' => $mock])
->assertDontSee('Start Proxy');
});