refactor(auth): enforce authorization checks across livewire components

Add authorization checks to multiple Livewire components to ensure users
have proper permissions before performing sensitive operations. This includes:

- Adding AuthorizesRequests trait to components handling deployments, backups,
  services, and configuration uploads
- Enforcing 'deploy', 'update', and 'manageBackups' authorization checks
- Adding instance admin check for system upgrade operations
- Improving database queries with team ownership scope
- Moving backup trigger from component to button with new backupNow() method
This commit is contained in:
Andras Bacsai 2026-02-27 11:59:26 +01:00
parent cebef8e258
commit 68f81df0bb
10 changed files with 105 additions and 30 deletions

View file

@ -45,6 +45,9 @@ public function back()
public function submitSearch() public function submitSearch()
{ {
if (Auth::id() !== 0 && ! session('impersonating')) {
return redirect()->route('dashboard');
}
if ($this->search !== '') { if ($this->search !== '') {
$this->foundUsers = User::where(function ($query) { $this->foundUsers = User::where(function ($query) {
$query->where('name', 'like', "%{$this->search}%") $query->where('name', 'like', "%{$this->search}%")
@ -55,6 +58,9 @@ public function submitSearch()
public function getSubscribers() public function getSubscribers()
{ {
if (Auth::id() !== 0 && ! session('impersonating')) {
return redirect()->route('dashboard');
}
$this->inactiveSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', false)->count(); $this->inactiveSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', false)->count();
$this->activeSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', true)->count(); $this->activeSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', true)->count();
} }

View file

@ -52,6 +52,7 @@ public function show_debug()
public function force_start() public function force_start()
{ {
try { try {
$this->authorize('deploy', $this->application);
force_start_deployment($this->application_deployment_queue); force_start_deployment($this->application_deployment_queue);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
@ -82,6 +83,11 @@ public function copyLogsToClipboard(): string
public function cancel() public function cancel()
{ {
try {
$this->authorize('deploy', $this->application);
} catch (\Throwable $e) {
return handleError($e, $this);
}
$deployment_uuid = $this->application_deployment_queue->deployment_uuid; $deployment_uuid = $this->application_deployment_queue->deployment_uuid;
$kill_command = "docker rm -f {$deployment_uuid}"; $kill_command = "docker rm -f {$deployment_uuid}";
$build_server_id = $this->application_deployment_queue->build_server_id ?? $this->application->destination->server_id; $build_server_id = $this->application_deployment_queue->build_server_id ?? $this->application->destination->server_id;

View file

@ -3,11 +3,14 @@
namespace App\Livewire\Project\Application; namespace App\Livewire\Project\Application;
use App\Models\Application; use App\Models\Application;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Validate; use Livewire\Attributes\Validate;
use Livewire\Component; use Livewire\Component;
class Swarm extends Component class Swarm extends Component
{ {
use AuthorizesRequests;
public Application $application; public Application $application;
#[Validate('required')] #[Validate('required')]
@ -51,6 +54,7 @@ public function syncData(bool $toModel = false)
public function instantSave() public function instantSave()
{ {
try { try {
$this->authorize('update', $this->application);
$this->syncData(true); $this->syncData(true);
$this->dispatch('success', 'Swarm settings updated.'); $this->dispatch('success', 'Swarm settings updated.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
@ -61,6 +65,7 @@ public function instantSave()
public function submit() public function submit()
{ {
try { try {
$this->authorize('update', $this->application);
$this->syncData(true); $this->syncData(true);
$this->dispatch('success', 'Swarm settings updated.'); $this->dispatch('success', 'Swarm settings updated.');
} catch (\Throwable $e) { } catch (\Throwable $e) {

View file

@ -201,6 +201,18 @@ public function delete($password)
} }
} }
public function backupNow()
{
try {
$this->authorize('manageBackups', $this->backup->database);
\App\Jobs\DatabaseBackupJob::dispatch($this->backup);
$this->dispatch('success', 'Backup queued. It will be available in a few minutes.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function instantSave() public function instantSave()
{ {
try { try {

View file

@ -3,12 +3,15 @@
namespace App\Livewire\Project\Database; namespace App\Livewire\Project\Database;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Livewire\Component; use Livewire\Component;
class BackupExecutions extends Component class BackupExecutions extends Component
{ {
use AuthorizesRequests;
public ?ScheduledDatabaseBackup $backup = null; public ?ScheduledDatabaseBackup $backup = null;
public $database; public $database;
@ -44,29 +47,45 @@ public function getListeners()
public function cleanupFailed() public function cleanupFailed()
{ {
if ($this->backup) { try {
$this->backup->executions()->where('status', 'failed')->delete(); $this->authorize('manageBackups', $this->database);
$this->refreshBackupExecutions(); if ($this->backup) {
$this->dispatch('success', 'Failed backups cleaned up.'); $this->backup->executions()->where('status', 'failed')->delete();
$this->refreshBackupExecutions();
$this->dispatch('success', 'Failed backups cleaned up.');
}
} catch (\Throwable $e) {
return handleError($e, $this);
} }
} }
public function cleanupDeleted() public function cleanupDeleted()
{ {
if ($this->backup) { try {
$deletedCount = $this->backup->executions()->where('local_storage_deleted', true)->count(); $this->authorize('manageBackups', $this->database);
if ($deletedCount > 0) { if ($this->backup) {
$this->backup->executions()->where('local_storage_deleted', true)->delete(); $deletedCount = $this->backup->executions()->where('local_storage_deleted', true)->count();
$this->refreshBackupExecutions(); if ($deletedCount > 0) {
$this->dispatch('success', "Cleaned up {$deletedCount} backup entries deleted from local storage."); $this->backup->executions()->where('local_storage_deleted', true)->delete();
} else { $this->refreshBackupExecutions();
$this->dispatch('info', 'No backup entries found that are deleted from local storage.'); $this->dispatch('success', "Cleaned up {$deletedCount} backup entries deleted from local storage.");
} else {
$this->dispatch('info', 'No backup entries found that are deleted from local storage.');
}
} }
} catch (\Throwable $e) {
return handleError($e, $this);
} }
} }
public function deleteBackup($executionId, $password) public function deleteBackup($executionId, $password)
{ {
try {
$this->authorize('manageBackups', $this->database);
} catch (\Throwable $e) {
return handleError($e, $this);
}
if (! verifyPasswordConfirmation($password, $this)) { if (! verifyPasswordConfirmation($password, $this)) {
return; return;
} }

View file

@ -3,10 +3,13 @@
namespace App\Livewire\Project\Service; namespace App\Livewire\Project\Service;
use App\Models\Service; use App\Models\Service;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component; use Livewire\Component;
class EditCompose extends Component class EditCompose extends Component
{ {
use AuthorizesRequests;
public Service $service; public Service $service;
public $serviceId; public $serviceId;
@ -72,19 +75,29 @@ public function validateCompose()
public function saveEditedCompose() public function saveEditedCompose()
{ {
$this->dispatch('info', 'Saving new docker compose...'); try {
$this->dispatch('saveCompose', $this->dockerComposeRaw); $this->authorize('update', $this->service);
$this->dispatch('refreshStorages'); $this->dispatch('info', 'Saving new docker compose...');
$this->dispatch('saveCompose', $this->dockerComposeRaw);
$this->dispatch('refreshStorages');
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function instantSave() public function instantSave()
{ {
$this->validate([ try {
'isContainerLabelEscapeEnabled' => 'required', $this->authorize('update', $this->service);
]); $this->validate([
$this->syncData(true); 'isContainerLabelEscapeEnabled' => 'required',
$this->service->save(['is_container_label_escape_enabled' => $this->isContainerLabelEscapeEnabled]); ]);
$this->dispatch('success', 'Service updated successfully'); $this->syncData(true);
$this->service->save(['is_container_label_escape_enabled' => $this->isContainerLabelEscapeEnabled]);
$this->dispatch('success', 'Service updated successfully');
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function render() public function render()

View file

@ -4,12 +4,15 @@
use App\Models\Service; use App\Models\Service;
use App\Support\ValidationPatterns; use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Livewire\Component; use Livewire\Component;
class StackForm extends Component class StackForm extends Component
{ {
use AuthorizesRequests;
public Service $service; public Service $service;
public Collection $fields; public Collection $fields;
@ -128,14 +131,20 @@ public function saveCompose($raw)
public function instantSave() public function instantSave()
{ {
$this->syncData(true); try {
$this->service->save(); $this->authorize('update', $this->service);
$this->dispatch('success', 'Service settings saved.'); $this->syncData(true);
$this->service->save();
$this->dispatch('success', 'Service settings saved.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function submit($notify = true) public function submit($notify = true)
{ {
try { try {
$this->authorize('update', $this->service);
$this->validate(); $this->validate();
$this->syncData(true); $this->syncData(true);

View file

@ -3,10 +3,13 @@
namespace App\Livewire\Project\Shared; namespace App\Livewire\Project\Shared;
use App\Models\Application; use App\Models\Application;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component; use Livewire\Component;
class UploadConfig extends Component class UploadConfig extends Component
{ {
use AuthorizesRequests;
public $config; public $config;
public $applicationId; public $applicationId;
@ -29,13 +32,12 @@ public function mount()
public function uploadConfig() public function uploadConfig()
{ {
try { try {
$application = Application::findOrFail($this->applicationId); $application = Application::ownedByCurrentTeam()->findOrFail($this->applicationId);
$this->authorize('update', $application);
$application->setConfig($this->config); $application->setConfig($this->config);
$this->dispatch('success', 'Application settings updated'); $this->dispatch('success', 'Application settings updated');
} catch (\Exception $e) { } catch (\Throwable $e) {
$this->dispatch('error', $e->getMessage()); return handleError($e, $this);
return;
} }
} }

View file

@ -44,6 +44,9 @@ public function checkUpdate()
public function upgrade() public function upgrade()
{ {
try { try {
if (! isInstanceAdmin()) {
abort(403);
}
if ($this->updateInProgress) { if ($this->updateInProgress) {
return; return;
} }

View file

@ -5,7 +5,7 @@
Save Save
</x-forms.button> </x-forms.button>
@if (str($status)->startsWith('running')) @if (str($status)->startsWith('running'))
<livewire:project.database.backup-now :backup="$backup" /> <x-forms.button wire:click='backupNow'>Backup Now</x-forms.button>
@endif @endif
@if ($backup->database_id !== 0) @if ($backup->database_id !== 0)
<x-modal-confirmation title="Confirm Backup Schedule Deletion?" buttonTitle="Delete Backups and Schedule" <x-modal-confirmation title="Confirm Backup Schedule Deletion?" buttonTitle="Delete Backups and Schedule"