feat(auth): implement authorization checks for server updates across multiple components

- Added authorization checks using the `authorize` method in various Livewire components to ensure only authorized users can update server settings.
- Updated `ServerPolicy` to restrict update permissions to admin users and their respective teams.
- Enhanced security and access control for server management functionalities.
This commit is contained in:
Andras Bacsai
2025-08-22 13:02:11 +02:00
parent 0748ef3ee5
commit 3ffc751f1a
10 changed files with 47 additions and 3 deletions

View File

@@ -76,6 +76,7 @@ class Advanced extends Component
public function syncData(bool $toModel = false) public function syncData(bool $toModel = false)
{ {
if ($toModel) { if ($toModel) {
$this->authorize('update', $this->server);
$this->validate(); $this->validate();
$this->server->settings->concurrent_builds = $this->concurrentBuilds; $this->server->settings->concurrent_builds = $this->concurrentBuilds;
$this->server->settings->dynamic_timeout = $this->dynamicTimeout; $this->server->settings->dynamic_timeout = $this->dynamicTimeout;

View File

@@ -4,11 +4,14 @@ namespace App\Livewire\Server;
use App\Actions\Server\ConfigureCloudflared; use App\Actions\Server\ConfigureCloudflared;
use App\Models\Server; use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Validate; use Livewire\Attributes\Validate;
use Livewire\Component; use Livewire\Component;
class CloudflareTunnel extends Component class CloudflareTunnel extends Component
{ {
use AuthorizesRequests;
public Server $server; public Server $server;
#[Validate(['required', 'string'])] #[Validate(['required', 'string'])]
@@ -51,6 +54,7 @@ class CloudflareTunnel extends Component
public function toggleCloudflareTunnels() public function toggleCloudflareTunnels()
{ {
try { try {
$this->authorize('update', $this->server);
remote_process(['docker rm -f coolify-cloudflared'], $this->server, false, 10); remote_process(['docker rm -f coolify-cloudflared'], $this->server, false, 10);
$this->isCloudflareTunnelsEnabled = false; $this->isCloudflareTunnelsEnabled = false;
$this->server->settings->is_cloudflare_tunnel = false; $this->server->settings->is_cloudflare_tunnel = false;
@@ -68,6 +72,7 @@ class CloudflareTunnel extends Component
public function manualCloudflareConfig() public function manualCloudflareConfig()
{ {
$this->authorize('update', $this->server);
$this->isCloudflareTunnelsEnabled = true; $this->isCloudflareTunnelsEnabled = true;
$this->server->settings->is_cloudflare_tunnel = true; $this->server->settings->is_cloudflare_tunnel = true;
$this->server->settings->save(); $this->server->settings->save();
@@ -78,6 +83,7 @@ class CloudflareTunnel extends Component
public function automatedCloudflareConfig() public function automatedCloudflareConfig()
{ {
try { try {
$this->authorize('update', $this->server);
if (str($this->ssh_domain)->contains('https://')) { if (str($this->ssh_domain)->contains('https://')) {
$this->ssh_domain = str($this->ssh_domain)->replace('https://', '')->replace('http://', '')->trim(); $this->ssh_domain = str($this->ssh_domain)->replace('https://', '')->replace('http://', '')->trim();
$this->ssh_domain = str($this->ssh_domain)->replace('/', ''); $this->ssh_domain = str($this->ssh_domain)->replace('/', '');

View File

@@ -4,11 +4,14 @@ namespace App\Livewire\Server;
use App\Jobs\DockerCleanupJob; use App\Jobs\DockerCleanupJob;
use App\Models\Server; use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Validate; use Livewire\Attributes\Validate;
use Livewire\Component; use Livewire\Component;
class DockerCleanup extends Component class DockerCleanup extends Component
{ {
use AuthorizesRequests;
public Server $server; public Server $server;
public array $parameters = []; public array $parameters = [];
@@ -42,6 +45,7 @@ class DockerCleanup extends Component
public function syncData(bool $toModel = false) public function syncData(bool $toModel = false)
{ {
if ($toModel) { if ($toModel) {
$this->authorize('update', $this->server);
$this->validate(); $this->validate();
$this->server->settings->force_docker_cleanup = $this->forceDockerCleanup; $this->server->settings->force_docker_cleanup = $this->forceDockerCleanup;
$this->server->settings->docker_cleanup_frequency = $this->dockerCleanupFrequency; $this->server->settings->docker_cleanup_frequency = $this->dockerCleanupFrequency;
@@ -71,6 +75,7 @@ class DockerCleanup extends Component
public function manualCleanup() public function manualCleanup()
{ {
try { try {
$this->authorize('update', $this->server);
DockerCleanupJob::dispatch($this->server, true, $this->deleteUnusedVolumes, $this->deleteUnusedNetworks); DockerCleanupJob::dispatch($this->server, true, $this->deleteUnusedVolumes, $this->deleteUnusedNetworks);
$this->dispatch('success', 'Manual cleanup job started. Depending on the amount of data, this might take a while.'); $this->dispatch('success', 'Manual cleanup job started. Depending on the amount of data, this might take a while.');
} catch (\Throwable $e) { } catch (\Throwable $e) {

View File

@@ -5,11 +5,14 @@ namespace App\Livewire\Server;
use App\Actions\Server\StartLogDrain; use App\Actions\Server\StartLogDrain;
use App\Actions\Server\StopLogDrain; use App\Actions\Server\StopLogDrain;
use App\Models\Server; use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Validate; use Livewire\Attributes\Validate;
use Livewire\Component; use Livewire\Component;
class LogDrains extends Component class LogDrains extends Component
{ {
use AuthorizesRequests;
public Server $server; public Server $server;
#[Validate(['boolean'])] #[Validate(['boolean'])]
@@ -160,6 +163,7 @@ class LogDrains extends Component
public function instantSave() public function instantSave()
{ {
try { try {
$this->authorize('update', $this->server);
$this->syncData(true); $this->syncData(true);
if ($this->server->isLogDrainEnabled()) { if ($this->server->isLogDrainEnabled()) {
StartLogDrain::run($this->server); StartLogDrain::run($this->server);
@@ -176,6 +180,7 @@ class LogDrains extends Component
public function submit(string $type) public function submit(string $type)
{ {
try { try {
$this->authorize('update', $this->server);
$this->syncData(true, $type); $this->syncData(true, $type);
$this->dispatch('success', 'Settings saved.'); $this->dispatch('success', 'Settings saved.');
} catch (\Throwable $e) { } catch (\Throwable $e) {

View File

@@ -6,12 +6,15 @@ use App\Enums\ProxyTypes;
use App\Models\Server; use App\Models\Server;
use App\Models\Team; use App\Models\Team;
use App\Support\ValidationPatterns; use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Livewire\Attributes\Locked; use Livewire\Attributes\Locked;
use Livewire\Component; use Livewire\Component;
class ByIp extends Component class ByIp extends Component
{ {
use AuthorizesRequests;
#[Locked] #[Locked]
public $private_keys; public $private_keys;
@@ -115,6 +118,7 @@ class ByIp extends Component
{ {
$this->validate(); $this->validate();
try { try {
$this->authorize('create', Server::class);
if (Server::where('team_id', currentTeam()->id) if (Server::where('team_id', currentTeam()->id)
->where('ip', $this->ip) ->where('ip', $this->ip)
->exists()) { ->exists()) {

View File

@@ -4,10 +4,13 @@ namespace App\Livewire\Server\PrivateKey;
use App\Models\PrivateKey; use App\Models\PrivateKey;
use App\Models\Server; use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component; use Livewire\Component;
class Show extends Component class Show extends Component
{ {
use AuthorizesRequests;
public Server $server; public Server $server;
public $privateKeys = []; public $privateKeys = [];
@@ -35,6 +38,7 @@ class Show extends Component
$originalPrivateKeyId = $this->server->getOriginal('private_key_id'); $originalPrivateKeyId = $this->server->getOriginal('private_key_id');
try { try {
$this->authorize('update', $this->server);
$this->server->update(['private_key_id' => $privateKeyId]); $this->server->update(['private_key_id' => $privateKeyId]);
['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection(justCheckingNewKey: true); ['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection(justCheckingNewKey: true);
if ($uptime) { if ($uptime) {

View File

@@ -5,10 +5,13 @@ namespace App\Livewire\Server;
use App\Actions\Proxy\CheckConfiguration; use App\Actions\Proxy\CheckConfiguration;
use App\Actions\Proxy\SaveConfiguration; use App\Actions\Proxy\SaveConfiguration;
use App\Models\Server; use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component; use Livewire\Component;
class Proxy extends Component class Proxy extends Component
{ {
use AuthorizesRequests;
public Server $server; public Server $server;
public ?string $selectedProxy = null; public ?string $selectedProxy = null;
@@ -47,6 +50,7 @@ class Proxy extends Component
public function changeProxy() public function changeProxy()
{ {
$this->authorize('update', $this->server);
$this->server->proxy = null; $this->server->proxy = null;
$this->server->save(); $this->server->save();
@@ -56,6 +60,7 @@ class Proxy extends Component
public function selectProxy($proxy_type) public function selectProxy($proxy_type)
{ {
try { try {
$this->authorize('update', $this->server);
$this->server->changeProxy($proxy_type, async: false); $this->server->changeProxy($proxy_type, async: false);
$this->selectedProxy = $this->server->proxy->type; $this->selectedProxy = $this->server->proxy->type;
@@ -68,6 +73,7 @@ class Proxy extends Component
public function instantSave() public function instantSave()
{ {
try { try {
$this->authorize('update', $this->server);
$this->validate(); $this->validate();
$this->server->settings->save(); $this->server->settings->save();
$this->dispatch('success', 'Settings saved.'); $this->dispatch('success', 'Settings saved.');
@@ -79,6 +85,7 @@ class Proxy extends Component
public function instantSaveRedirect() public function instantSaveRedirect()
{ {
try { try {
$this->authorize('update', $this->server);
$this->server->proxy->redirect_enabled = $this->redirect_enabled; $this->server->proxy->redirect_enabled = $this->redirect_enabled;
$this->server->save(); $this->server->save();
$this->server->setupDefaultRedirect(); $this->server->setupDefaultRedirect();
@@ -91,6 +98,7 @@ class Proxy extends Component
public function submit() public function submit()
{ {
try { try {
$this->authorize('update', $this->server);
SaveConfiguration::run($this->server, $this->proxy_settings); SaveConfiguration::run($this->server, $this->proxy_settings);
$this->server->proxy->redirect_url = $this->redirect_url; $this->server->proxy->redirect_url = $this->redirect_url;
$this->server->save(); $this->server->save();
@@ -104,6 +112,7 @@ class Proxy extends Component
public function reset_proxy_configuration() public function reset_proxy_configuration()
{ {
try { try {
$this->authorize('update', $this->server);
$this->proxy_settings = CheckConfiguration::run($this->server, true); $this->proxy_settings = CheckConfiguration::run($this->server, true);
SaveConfiguration::run($this->server, $this->proxy_settings); SaveConfiguration::run($this->server, $this->proxy_settings);
$this->server->save(); $this->server->save();

View File

@@ -7,12 +7,15 @@ use App\Actions\Server\StopSentinel;
use App\Events\ServerReachabilityChanged; use App\Events\ServerReachabilityChanged;
use App\Models\Server; use App\Models\Server;
use App\Support\ValidationPatterns; use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Computed; use Livewire\Attributes\Computed;
use Livewire\Attributes\Locked; use Livewire\Attributes\Locked;
use Livewire\Component; use Livewire\Component;
class Show extends Component class Show extends Component
{ {
use AuthorizesRequests;
public Server $server; public Server $server;
public string $name; public string $name;
@@ -157,6 +160,8 @@ class Show extends Component
throw new \Exception('This IP/Domain is already in use by another server in your team.'); 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->name = $this->name;
$this->server->description = $this->description; $this->server->description = $this->description;
$this->server->ip = $this->ip; $this->server->ip = $this->ip;
@@ -220,6 +225,7 @@ class Show extends Component
public function validateServer($install = true) public function validateServer($install = true)
{ {
try { try {
$this->authorize('update', $this->server);
$this->validationLogs = $this->server->validation_logs = null; $this->validationLogs = $this->server->validation_logs = null;
$this->server->save(); $this->server->save();
$this->dispatch('init', $install); $this->dispatch('init', $install);

View File

@@ -5,10 +5,13 @@ namespace App\Livewire\Server;
use App\Actions\Proxy\CheckProxy; use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy; use App\Actions\Proxy\StartProxy;
use App\Models\Server; use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component; use Livewire\Component;
class ValidateAndInstall extends Component class ValidateAndInstall extends Component
{ {
use AuthorizesRequests;
public Server $server; public Server $server;
public int $number_of_tries = 0; public int $number_of_tries = 0;
@@ -62,6 +65,7 @@ class ValidateAndInstall extends Component
public function validateConnection() public function validateConnection()
{ {
$this->authorize('update', $this->server);
['uptime' => $this->uptime, 'error' => $error] = $this->server->validateConnection(); ['uptime' => $this->uptime, 'error' => $error] = $this->server->validateConnection();
if (! $this->uptime) { if (! $this->uptime) {
$this->error = 'Server is not reachable. Please validate your configuration and connection.<br>Check this <a target="_blank" class="text-black underline dark:text-white" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help. <br><br><div class="text-error">Error: '.$error.'</div>'; $this->error = 'Server is not reachable. Please validate your configuration and connection.<br>Check this <a target="_blank" class="text-black underline dark:text-white" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help. <br><br><div class="text-error">Error: '.$error.'</div>';

View File

@@ -28,7 +28,7 @@ class ServerPolicy
*/ */
public function create(User $user): bool public function create(User $user): bool
{ {
return true; return $user->isAdmin();
} }
/** /**
@@ -36,7 +36,7 @@ class ServerPolicy
*/ */
public function update(User $user, Server $server): bool public function update(User $user, Server $server): bool
{ {
return $user->teams()->get()->firstWhere('id', $server->team_id) !== null; return $user->isAdmin() && $user->teams()->get()->firstWhere('id', $server->team_id) !== null;
} }
/** /**
@@ -44,7 +44,7 @@ class ServerPolicy
*/ */
public function delete(User $user, Server $server): bool public function delete(User $user, Server $server): bool
{ {
return $user->teams()->get()->firstWhere('id', $server->team_id) !== null; return $user->isAdmin() && $user->teams()->get()->firstWhere('id', $server->team_id) !== null;
} }
/** /**