From 6772cfe603276ecbb580c58102549ecf70c09299 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 22 Aug 2025 14:04:25 +0200 Subject: [PATCH] feat(auth): implement authorization for Docker and server management - Added authorization checks in Livewire components related to Docker and server management to ensure only authorized users can create, update, and manage Docker instances and server settings. - Introduced new policies for StandaloneDocker and SwarmDocker to define access control rules based on user roles and team associations. - Updated AuthServiceProvider to register the new policies, enhancing security and access control for Docker functionalities and server management operations. --- app/Livewire/Destination/New/Docker.php | 4 ++ app/Livewire/Server/CaCertificate/Show.php | 5 ++ app/Livewire/Server/Destinations.php | 5 ++ app/Livewire/Server/Navbar.php | 7 ++ .../Server/Proxy/NewDynamicConfiguration.php | 9 ++- app/Livewire/Server/Security/Patches.php | 7 +- app/Livewire/Server/Show.php | 56 +++++++++++----- app/Livewire/Terminal/Index.php | 4 +- app/Policies/ServerPolicy.php | 40 ++++++++++++ app/Policies/StandaloneDockerPolicy.php | 65 +++++++++++++++++++ app/Policies/SwarmDockerPolicy.php | 65 +++++++++++++++++++ app/Providers/AuthServiceProvider.php | 2 + .../proxy/dynamic-configurations.blade.php | 3 +- 13 files changed, 244 insertions(+), 28 deletions(-) create mode 100644 app/Policies/StandaloneDockerPolicy.php create mode 100644 app/Policies/SwarmDockerPolicy.php diff --git a/app/Livewire/Destination/New/Docker.php b/app/Livewire/Destination/New/Docker.php index eb768d191..819ac3ecd 100644 --- a/app/Livewire/Destination/New/Docker.php +++ b/app/Livewire/Destination/New/Docker.php @@ -5,6 +5,7 @@ namespace App\Livewire\Destination\New; use App\Models\Server; use App\Models\StandaloneDocker; use App\Models\SwarmDocker; +use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Attributes\Locked; use Livewire\Attributes\Validate; use Livewire\Component; @@ -12,6 +13,8 @@ use Visus\Cuid2\Cuid2; class Docker extends Component { + use AuthorizesRequests; + #[Locked] public $servers; @@ -67,6 +70,7 @@ class Docker extends Component public function submit() { try { + $this->authorize('create', StandaloneDocker::class); $this->validate(); if ($this->isSwarm) { $found = $this->selectedServer->swarmDockers()->where('network', $this->network)->first(); diff --git a/app/Livewire/Server/CaCertificate/Show.php b/app/Livewire/Server/CaCertificate/Show.php index 750ed9f81..039b5f71d 100644 --- a/app/Livewire/Server/CaCertificate/Show.php +++ b/app/Livewire/Server/CaCertificate/Show.php @@ -6,12 +6,15 @@ use App\Helpers\SslHelper; use App\Jobs\RegenerateSslCertJob; use App\Models\Server; use App\Models\SslCertificate; +use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Support\Carbon; use Livewire\Attributes\Locked; use Livewire\Component; class Show extends Component { + use AuthorizesRequests; + #[Locked] public Server $server; @@ -52,6 +55,7 @@ class Show extends Component public function saveCaCertificate() { try { + $this->authorize('manageCaCertificate', $this->server); if (! $this->certificateContent) { throw new \Exception('Certificate content cannot be empty.'); } @@ -82,6 +86,7 @@ class Show extends Component public function regenerateCaCertificate() { try { + $this->authorize('manageCaCertificate', $this->server); SslHelper::generateSslCertificate( commonName: 'Coolify CA Certificate', serverId: $this->server->id, diff --git a/app/Livewire/Server/Destinations.php b/app/Livewire/Server/Destinations.php index dbab6e03f..3dbb3fcf8 100644 --- a/app/Livewire/Server/Destinations.php +++ b/app/Livewire/Server/Destinations.php @@ -5,11 +5,14 @@ namespace App\Livewire\Server; use App\Models\Server; use App\Models\StandaloneDocker; use App\Models\SwarmDocker; +use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Support\Collection; use Livewire\Component; class Destinations extends Component { + use AuthorizesRequests; + public Server $server; public Collection $networks; @@ -33,6 +36,7 @@ class Destinations extends Component public function add($name) { if ($this->server->isSwarm()) { + $this->authorize('create', SwarmDocker::class); $found = $this->server->swarmDockers()->where('network', $name)->first(); if ($found) { $this->dispatch('error', 'Network already added to this server.'); @@ -46,6 +50,7 @@ class Destinations extends Component ]); } } else { + $this->authorize('create', StandaloneDocker::class); $found = $this->server->standaloneDockers()->where('network', $name)->first(); if ($found) { $this->dispatch('error', 'Network already added to this server.'); diff --git a/app/Livewire/Server/Navbar.php b/app/Livewire/Server/Navbar.php index 5381d1e19..055290580 100644 --- a/app/Livewire/Server/Navbar.php +++ b/app/Livewire/Server/Navbar.php @@ -8,10 +8,13 @@ use App\Actions\Proxy\StopProxy; use App\Jobs\RestartProxyJob; use App\Models\Server; use App\Services\ProxyDashboardCacheService; +use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Component; class Navbar extends Component { + use AuthorizesRequests; + public Server $server; public bool $isChecking = false; @@ -57,6 +60,7 @@ class Navbar extends Component public function restart() { try { + $this->authorize('manageProxy', $this->server); RestartProxyJob::dispatch($this->server); } catch (\Throwable $e) { return handleError($e, $this); @@ -66,6 +70,7 @@ class Navbar extends Component public function checkProxy() { try { + $this->authorize('manageProxy', $this->server); CheckProxy::run($this->server, true); $this->dispatch('startProxy')->self(); } catch (\Throwable $e) { @@ -76,6 +81,7 @@ class Navbar extends Component public function startProxy() { try { + $this->authorize('manageProxy', $this->server); $activity = StartProxy::run($this->server, force: true); $this->dispatch('activityMonitor', $activity->id); } catch (\Throwable $e) { @@ -86,6 +92,7 @@ class Navbar extends Component public function stop(bool $forceStop = true) { try { + $this->authorize('manageProxy', $this->server); StopProxy::dispatch($this->server, $forceStop); } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Livewire/Server/Proxy/NewDynamicConfiguration.php b/app/Livewire/Server/Proxy/NewDynamicConfiguration.php index 2155f1e82..eb2db1cbb 100644 --- a/app/Livewire/Server/Proxy/NewDynamicConfiguration.php +++ b/app/Livewire/Server/Proxy/NewDynamicConfiguration.php @@ -4,11 +4,14 @@ namespace App\Livewire\Server\Proxy; use App\Enums\ProxyTypes; use App\Models\Server; +use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Component; use Symfony\Component\Yaml\Yaml; class NewDynamicConfiguration extends Component { + use AuthorizesRequests; + public string $fileName = ''; public string $value = ''; @@ -23,6 +26,7 @@ class NewDynamicConfiguration extends Component public function mount() { + $this->server = Server::ownedByCurrentTeam()->whereId($this->server_id)->first(); $this->parameters = get_route_parameters(); if ($this->fileName !== '') { $this->fileName = str_replace('|', '.', $this->fileName); @@ -32,6 +36,7 @@ class NewDynamicConfiguration extends Component public function addDynamicConfiguration() { try { + $this->authorize('update', $this->server); $this->validate([ 'fileName' => 'required', 'value' => 'required', @@ -39,9 +44,7 @@ class NewDynamicConfiguration extends Component if (data_get($this->parameters, 'server_uuid')) { $this->server = Server::ownedByCurrentTeam()->whereUuid(data_get($this->parameters, 'server_uuid'))->first(); } - if (! is_null($this->server_id)) { - $this->server = Server::ownedByCurrentTeam()->whereId($this->server_id)->first(); - } + if (is_null($this->server)) { return redirect()->route('server.index'); } diff --git a/app/Livewire/Server/Security/Patches.php b/app/Livewire/Server/Security/Patches.php index eca4bbcd6..ae114c224 100644 --- a/app/Livewire/Server/Security/Patches.php +++ b/app/Livewire/Server/Security/Patches.php @@ -7,10 +7,13 @@ use App\Actions\Server\UpdatePackage; use App\Events\ServerPackageUpdated; use App\Models\Server; use App\Notifications\Server\ServerPatchCheck; +use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Component; class Patches extends Component { + use AuthorizesRequests; + public array $parameters; public Server $server; @@ -36,11 +39,9 @@ class Patches extends Component public function mount() { - if (! auth()->user()->isAdmin()) { - abort(403); - } $this->parameters = get_route_parameters(); $this->server = Server::ownedByCurrentTeam()->whereUuid($this->parameters['server_uuid'])->firstOrFail(); + $this->authorize('viewSecurity', $this->server); } public function checkForUpdatesDispatch() diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php index db2cef880..70c5be5db 100644 --- a/app/Livewire/Server/Show.php +++ b/app/Livewire/Server/Show.php @@ -152,6 +152,7 @@ class Show extends Component if ($toModel) { $this->validate(); + $this->authorize('update', $this->server); if (Server::where('team_id', currentTeam()->id) ->where('ip', $this->ip) ->where('id', '!=', $this->server->id) @@ -160,8 +161,6 @@ class Show extends Component throw new \Exception('This IP/Domain is already in use by another server in your team.'); } - $this->authorize('update', $this->server); - $this->server->name = $this->name; $this->server->description = $this->description; $this->server->ip = $this->ip; @@ -253,38 +252,57 @@ class Show extends Component public function restartSentinel() { - $this->server->restartSentinel(); - $this->dispatch('success', 'Sentinel restarted.'); + try { + $this->authorize('manageSentinel', $this->server); + $this->server->restartSentinel(); + $this->dispatch('success', 'Sentinel restarted.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } public function updatedIsSentinelDebugEnabled($value) { - $this->submit(); - $this->restartSentinel(); + try { + $this->submit(); + $this->restartSentinel(); + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function updatedIsMetricsEnabled($value) { - $this->submit(); - $this->restartSentinel(); + try { + $this->submit(); + $this->restartSentinel(); + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function updatedIsSentinelEnabled($value) { - if ($value === true) { - StartSentinel::run($this->server, true); - } else { - $this->isMetricsEnabled = false; - $this->isSentinelDebugEnabled = false; - StopSentinel::dispatch($this->server); + try { + $this->authorize('manageSentinel', $this->server); + if ($value === true) { + StartSentinel::run($this->server, true); + } else { + $this->isMetricsEnabled = false; + $this->isSentinelDebugEnabled = false; + StopSentinel::dispatch($this->server); + } + $this->submit(); + } catch (\Throwable $e) { + return handleError($e, $this); } - $this->submit(); - } public function regenerateSentinelToken() { try { + $this->authorize('manageSentinel', $this->server); $this->server->settings->generateSentinelToken(); $this->dispatch('success', 'Token regenerated & Sentinel restarted.'); } catch (\Throwable $e) { @@ -294,7 +312,11 @@ class Show extends Component public function instantSave() { - $this->submit(); + try { + $this->submit(); + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function submit() diff --git a/app/Livewire/Terminal/Index.php b/app/Livewire/Terminal/Index.php index f4008cb1c..dfeb5da66 100644 --- a/app/Livewire/Terminal/Index.php +++ b/app/Livewire/Terminal/Index.php @@ -18,9 +18,7 @@ class Index extends Component public function mount() { - if (! auth()->user()->isAdmin()) { - abort(403); - } + $this->authorize('useTerminal', Server::class); $this->servers = Server::isReachable()->get()->filter(function ($server) { return $server->isTerminalEnabled(); }); diff --git a/app/Policies/ServerPolicy.php b/app/Policies/ServerPolicy.php index bf3c1bd30..a9a64e801 100644 --- a/app/Policies/ServerPolicy.php +++ b/app/Policies/ServerPolicy.php @@ -62,4 +62,44 @@ class ServerPolicy { return false; } + + /** + * Determine whether the user can manage proxy (start/stop/restart). + */ + public function manageProxy(User $user, Server $server): bool + { + return $user->isAdmin() && $user->teams()->get()->firstWhere('id', $server->team_id) !== null; + } + + /** + * Determine whether the user can manage sentinel (start/stop). + */ + public function manageSentinel(User $user, Server $server): bool + { + return $user->isAdmin() && $user->teams()->get()->firstWhere('id', $server->team_id) !== null; + } + + /** + * Determine whether the user can manage CA certificates. + */ + public function manageCaCertificate(User $user, Server $server): bool + { + return $user->isAdmin() && $user->teams()->get()->firstWhere('id', $server->team_id) !== null; + } + + /** + * Determine whether the user can view terminal. + */ + public function viewTerminal(User $user, Server $server): bool + { + return $user->isAdmin() && $user->teams()->get()->firstWhere('id', $server->team_id) !== null; + } + + /** + * Determine whether the user can view security views. + */ + public function viewSecurity(User $user, Server $server): bool + { + return $user->isAdmin() && $user->teams()->get()->firstWhere('id', $server->team_id) !== null; + } } diff --git a/app/Policies/StandaloneDockerPolicy.php b/app/Policies/StandaloneDockerPolicy.php new file mode 100644 index 000000000..08d7ea6fe --- /dev/null +++ b/app/Policies/StandaloneDockerPolicy.php @@ -0,0 +1,65 @@ +teams()->get()->firstWhere('id', $standaloneDocker->server->team_id) !== null; + } + + /** + * Determine whether the user can create models. + */ + public function create(User $user): bool + { + return $user->isAdmin(); + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, StandaloneDocker $standaloneDocker): bool + { + return $user->isAdmin() && $user->teams()->get()->firstWhere('id', $standaloneDocker->server->team_id) !== null; + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, StandaloneDocker $standaloneDocker): bool + { + return $user->isAdmin() && $user->teams()->get()->firstWhere('id', $standaloneDocker->server->team_id) !== null; + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, StandaloneDocker $standaloneDocker): bool + { + return false; + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, StandaloneDocker $standaloneDocker): bool + { + return false; + } +} diff --git a/app/Policies/SwarmDockerPolicy.php b/app/Policies/SwarmDockerPolicy.php new file mode 100644 index 000000000..94ea7a1ee --- /dev/null +++ b/app/Policies/SwarmDockerPolicy.php @@ -0,0 +1,65 @@ +teams()->get()->firstWhere('id', $swarmDocker->server->team_id) !== null; + } + + /** + * Determine whether the user can create models. + */ + public function create(User $user): bool + { + return $user->isAdmin(); + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, SwarmDocker $swarmDocker): bool + { + return $user->isAdmin() && $user->teams()->get()->firstWhere('id', $swarmDocker->server->team_id) !== null; + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, SwarmDocker $swarmDocker): bool + { + return $user->isAdmin() && $user->teams()->get()->firstWhere('id', $swarmDocker->server->team_id) !== null; + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, SwarmDocker $swarmDocker): bool + { + return false; + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, SwarmDocker $swarmDocker): bool + { + return false; + } +} diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 476e064d6..30b7cc3c0 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -15,6 +15,8 @@ class AuthServiceProvider extends ServiceProvider protected $policies = [ \App\Models\Server::class => \App\Policies\ServerPolicy::class, \App\Models\PrivateKey::class => \App\Policies\PrivateKeyPolicy::class, + \App\Models\StandaloneDocker::class => \App\Policies\StandaloneDockerPolicy::class, + \App\Models\SwarmDocker::class => \App\Policies\SwarmDockerPolicy::class, ]; /** diff --git a/resources/views/livewire/server/proxy/dynamic-configurations.blade.php b/resources/views/livewire/server/proxy/dynamic-configurations.blade.php index ebb660746..53ff99ff9 100644 --- a/resources/views/livewire/server/proxy/dynamic-configurations.blade.php +++ b/resources/views/livewire/server/proxy/dynamic-configurations.blade.php @@ -7,14 +7,13 @@ @if ($server->isFunctional())
-

Dynamic Configurations

Reload - +
You can add dynamic proxy configurations here.