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:
parent
fcc58ca08a
commit
41e1248b6f
2 changed files with 80 additions and 127 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue