From d2deaa8363e0e38956670732b864c64fc749b8b7 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 15 Jun 2026 12:31:30 +0200 Subject: [PATCH] fix(auth): restrict Sentinel access and register S3 policy --- app/Livewire/Server/Sentinel/Logs.php | 7 +++ app/Livewire/Server/Sentinel/Show.php | 7 +++ app/Policies/ServerPolicy.php | 8 +++ app/Providers/AuthServiceProvider.php | 5 ++ .../server/sidebar-sentinel.blade.php | 18 ++++--- .../views/livewire/server/navbar.blade.php | 2 +- .../Authorization/ServerAuthorizationTest.php | 43 ++++++++++++++- .../S3StoragePolicyRegistrationTest.php | 54 +++++++++++++++++++ tests/Unit/Policies/ServerPolicyTest.php | 12 +++++ 9 files changed, 146 insertions(+), 10 deletions(-) create mode 100644 tests/Feature/S3StoragePolicyRegistrationTest.php diff --git a/app/Livewire/Server/Sentinel/Logs.php b/app/Livewire/Server/Sentinel/Logs.php index 6619e101e..1190cd59a 100644 --- a/app/Livewire/Server/Sentinel/Logs.php +++ b/app/Livewire/Server/Sentinel/Logs.php @@ -3,11 +3,14 @@ namespace App\Livewire\Server\Sentinel; use App\Models\Server; +use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\View\View; use Livewire\Component; class Logs extends Component { + use AuthorizesRequests; + public ?Server $server = null; public array $parameters = []; @@ -19,7 +22,11 @@ public function mount(): void $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->firstOrFail(); } catch (\Throwable $e) { handleError($e, $this); + + return; } + + $this->authorize('viewSentinel', $this->server); } public function render(): View diff --git a/app/Livewire/Server/Sentinel/Show.php b/app/Livewire/Server/Sentinel/Show.php index 7070a09ce..fc30994d6 100644 --- a/app/Livewire/Server/Sentinel/Show.php +++ b/app/Livewire/Server/Sentinel/Show.php @@ -3,11 +3,14 @@ namespace App\Livewire\Server\Sentinel; use App\Models\Server; +use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\View\View; use Livewire\Component; class Show extends Component { + use AuthorizesRequests; + public ?Server $server = null; public array $parameters = []; @@ -19,7 +22,11 @@ public function mount(): void $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->firstOrFail(); } catch (\Throwable $e) { handleError($e, $this); + + return; } + + $this->authorize('viewSentinel', $this->server); } public function render(): View diff --git a/app/Policies/ServerPolicy.php b/app/Policies/ServerPolicy.php index 32436987c..c1369ce67 100644 --- a/app/Policies/ServerPolicy.php +++ b/app/Policies/ServerPolicy.php @@ -79,6 +79,14 @@ public function manageSentinel(User $user, Server $server): bool return $user->isAdminOfTeam($server->team_id); } + /** + * Determine whether the user can view Sentinel configuration and logs. + */ + public function viewSentinel(User $user, Server $server): bool + { + return $user->isAdminOfTeam($server->team_id); + } + /** * Determine whether the user can manage CA certificates. */ diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 1ae539174..5c1a79cf7 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -17,6 +17,7 @@ use App\Models\PrivateKey; use App\Models\Project; use App\Models\PushoverNotificationSettings; +use App\Models\S3Storage; use App\Models\Server; use App\Models\Service; use App\Models\ServiceApplication; @@ -51,6 +52,7 @@ use App\Policies\PrivateKeyPolicy; use App\Policies\ProjectPolicy; use App\Policies\ResourceCreatePolicy; +use App\Policies\S3StoragePolicy; use App\Policies\ServerPolicy; use App\Policies\ServiceApplicationPolicy; use App\Policies\ServiceDatabasePolicy; @@ -109,6 +111,9 @@ class AuthServiceProvider extends ServiceProvider // Instance settings policy InstanceSettings::class => InstanceSettingsPolicy::class, + // S3 storage policy + S3Storage::class => S3StoragePolicy::class, + // Team policy Team::class => TeamPolicy::class, diff --git a/resources/views/components/server/sidebar-sentinel.blade.php b/resources/views/components/server/sidebar-sentinel.blade.php index 8125fe22c..421a1ec9d 100644 --- a/resources/views/components/server/sidebar-sentinel.blade.php +++ b/resources/views/components/server/sidebar-sentinel.blade.php @@ -1,10 +1,12 @@ diff --git a/resources/views/livewire/server/navbar.blade.php b/resources/views/livewire/server/navbar.blade.php index b49c04f5f..463ecef3b 100644 --- a/resources/views/livewire/server/navbar.blade.php +++ b/resources/views/livewire/server/navbar.blade.php @@ -78,7 +78,7 @@ class="flex items-center gap-6 overflow-x-scroll sm:overflow-x-hidden scrollbar @endif @endif - @if ($server->isFunctional() && !$server->isSwarm() && !$server->settings->is_build_server) + @if ($server->isFunctional() && !$server->isSwarm() && !$server->settings->is_build_server && auth()->user()?->can('viewSentinel', $server)) diff --git a/tests/Feature/Authorization/ServerAuthorizationTest.php b/tests/Feature/Authorization/ServerAuthorizationTest.php index 80b7aed40..ffcb4a1c6 100644 --- a/tests/Feature/Authorization/ServerAuthorizationTest.php +++ b/tests/Feature/Authorization/ServerAuthorizationTest.php @@ -1,5 +1,6 @@ 0]); + $this->withoutMiddleware(PreventRequestsDuringMaintenance::class); + + InstanceSettings::unguarded(fn () => InstanceSettings::updateOrCreate(['id' => 0], ['id' => 0])); $this->team = Team::factory()->create(); @@ -217,6 +220,44 @@ $this->get("/server/{$this->server->uuid}")->assertSuccessful(); }); +test('admin can access sentinel configuration and logs pages', function () { + $this->actingAs($this->admin); + session(['currentTeam' => $this->team]); + + $this->get(route('server.sentinel', ['server_uuid' => $this->server->uuid])) + ->assertSuccessful(); + + $this->get(route('server.sentinel.logs', ['server_uuid' => $this->server->uuid])) + ->assertSuccessful(); +}); + +test('member cannot access sentinel configuration and logs pages', function () { + $this->actingAs($this->member); + session(['currentTeam' => $this->team]); + + $this->get(route('server.sentinel', ['server_uuid' => $this->server->uuid])) + ->assertForbidden(); + + $this->get(route('server.sentinel.logs', ['server_uuid' => $this->server->uuid])) + ->assertForbidden(); +}); + +test('sentinel navigation is only visible to team admins', function () { + $this->actingAs($this->member); + session(['currentTeam' => $this->team]); + + $this->get("/server/{$this->server->uuid}") + ->assertSuccessful() + ->assertDontSee(route('server.sentinel', ['server_uuid' => $this->server->uuid])); + + $this->actingAs($this->admin); + session(['currentTeam' => $this->team]); + + $this->get("/server/{$this->server->uuid}") + ->assertSuccessful() + ->assertSee(route('server.sentinel', ['server_uuid' => $this->server->uuid])); +}); + test('unauthenticated user cannot access server page', function () { $this->get("/server/{$this->server->uuid}")->assertRedirect('/login'); }); diff --git a/tests/Feature/S3StoragePolicyRegistrationTest.php b/tests/Feature/S3StoragePolicyRegistrationTest.php new file mode 100644 index 000000000..f75c58a85 --- /dev/null +++ b/tests/Feature/S3StoragePolicyRegistrationTest.php @@ -0,0 +1,54 @@ +toBeInstanceOf(S3StoragePolicy::class); +}); + +test('s3 storage create ability is enforced through the registered policy', function () { + $team = Team::factory()->create(); + + $admin = User::factory()->create(); + $admin->teams()->attach($team, ['role' => 'admin']); + + $member = User::factory()->create(); + $member->teams()->attach($team, ['role' => 'member']); + + $this->actingAs($admin); + session(['currentTeam' => $team]); + + expect($admin->can('create', S3Storage::class))->toBeTrue() + ->and($member->can('create', S3Storage::class))->toBeFalse(); +}); + +test('s3 storage validate connection ability is enforced through the registered policy', function () { + $team = Team::factory()->create(); + + $admin = User::factory()->create(); + $admin->teams()->attach($team, ['role' => 'admin']); + + $member = User::factory()->create(); + $member->teams()->attach($team, ['role' => 'member']); + + $storage = S3Storage::create([ + 'team_id' => $team->id, + 'name' => 'Backups', + 'description' => 'Team backup storage', + 'region' => 'us-east-1', + 'key' => 'access-key', + 'secret' => 'secret-key', + 'bucket' => 'coolify-backups', + 'endpoint' => 'https://s3.us-east-1.amazonaws.com', + ]); + + expect($admin->can('validateConnection', $storage))->toBeTrue() + ->and($member->can('validateConnection', $storage))->toBeFalse(); +}); diff --git a/tests/Unit/Policies/ServerPolicyTest.php b/tests/Unit/Policies/ServerPolicyTest.php index afa64d090..d9084694a 100644 --- a/tests/Unit/Policies/ServerPolicyTest.php +++ b/tests/Unit/Policies/ServerPolicyTest.php @@ -152,6 +152,18 @@ $policy = new ServerPolicy; expect($policy->manageSentinel($user, $server))->toBeTrue(); + expect($policy->viewSentinel($user, $server))->toBeTrue(); expect($policy->manageCaCertificate($user, $server))->toBeTrue(); expect($policy->viewSecurity($user, $server))->toBeTrue(); }); + +it('denies team member to view sentinel', function () { + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false); + + $server = Mockery::mock(Server::class)->makePartial(); + $server->team_id = 1; + + $policy = new ServerPolicy; + expect($policy->viewSentinel($user, $server))->toBeFalse(); +});