feat(auth): implement authorization checks for database management

This commit is contained in:
Andras Bacsai
2025-08-23 18:50:35 +02:00
parent 6d02f6a60b
commit adb8f9d88e
17 changed files with 281 additions and 27 deletions

View File

@@ -5,6 +5,7 @@ namespace App\Livewire\Project\Database;
use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Attributes\Locked;
@@ -14,6 +15,8 @@ use Spatie\Url\Url;
class BackupEdit extends Component
{
use AuthorizesRequests;
public ScheduledDatabaseBackup $backup;
#[Locked]
@@ -129,6 +132,8 @@ class BackupEdit extends Component
public function delete($password)
{
$this->authorize('manageBackups', $this->backup->database);
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
@@ -186,6 +191,8 @@ class BackupEdit extends Component
public function instantSave()
{
try {
$this->authorize('manageBackups', $this->backup->database);
$this->syncData(true);
$this->dispatch('success', 'Backup updated successfully.');
} catch (\Throwable $e) {
@@ -214,6 +221,8 @@ class BackupEdit extends Component
public function submit()
{
try {
$this->authorize('manageBackups', $this->backup->database);
$this->syncData(true);
$this->dispatch('success', 'Backup updated successfully.');
} catch (\Throwable $e) {

View File

@@ -3,14 +3,19 @@
namespace App\Livewire\Project\Database;
use App\Jobs\DatabaseBackupJob;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
class BackupNow extends Component
{
use AuthorizesRequests;
public $backup;
public function backupNow()
{
$this->authorize('manageBackups', $this->backup->database);
DatabaseBackupJob::dispatch($this->backup);
$this->dispatch('success', 'Backup queued. It will be available in a few minutes.');
}

View File

@@ -8,11 +8,14 @@ use App\Models\Server;
use App\Models\StandaloneClickhouse;
use App\Support\ValidationPatterns;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class General extends Component
{
use AuthorizesRequests;
public Server $server;
public StandaloneClickhouse $database;
@@ -131,6 +134,8 @@ class General extends Component
public function instantSaveAdvanced()
{
try {
$this->authorize('update', $this->database);
if (! $this->server->isLogDrainEnabled()) {
$this->isLogDrainEnabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -149,6 +154,8 @@ class General extends Component
public function instantSave()
{
try {
$this->authorize('update', $this->database);
if ($this->isPublic && ! $this->publicPort) {
$this->dispatch('error', 'Public port is required.');
$this->isPublic = false;
@@ -186,6 +193,8 @@ class General extends Component
public function submit()
{
try {
$this->authorize('update', $this->database);
if (str($this->publicPort)->isEmpty()) {
$this->publicPort = null;
}

View File

@@ -26,27 +26,38 @@ class Configuration extends Component
public function mount()
{
$this->currentRoute = request()->route()->getName();
try {
$this->currentRoute = request()->route()->getName();
$project = currentTeam()
->projects()
->select('id', 'uuid', 'team_id')
->where('uuid', request()->route('project_uuid'))
->firstOrFail();
$environment = $project->environments()
->select('id', 'name', 'project_id', 'uuid')
->where('uuid', request()->route('environment_uuid'))
->firstOrFail();
$database = $environment->databases()
->where('uuid', request()->route('database_uuid'))
->firstOrFail();
$project = currentTeam()
->projects()
->select('id', 'uuid', 'team_id')
->where('uuid', request()->route('project_uuid'))
->firstOrFail();
$environment = $project->environments()
->select('id', 'name', 'project_id', 'uuid')
->where('uuid', request()->route('environment_uuid'))
->firstOrFail();
$database = $environment->databases()
->where('uuid', request()->route('database_uuid'))
->firstOrFail();
$this->database = $database;
$this->project = $project;
$this->environment = $environment;
if (str($this->database->status)->startsWith('running') && is_null($this->database->config_hash)) {
$this->database->isConfigurationChanged(true);
$this->dispatch('configurationChanged');
$this->database = $database;
$this->project = $project;
$this->environment = $environment;
if (str($this->database->status)->startsWith('running') && is_null($this->database->config_hash)) {
$this->database->isConfigurationChanged(true);
$this->dispatch('configurationChanged');
}
} catch (\Throwable $e) {
if ($e instanceof \Illuminate\Auth\Access\AuthorizationException) {
return redirect()->route('dashboard');
}
if ($e instanceof \Illuminate\Support\ItemNotFoundException) {
return redirect()->route('dashboard');
}
return handleError($e, $this);
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Livewire\Project\Database;
use App\Models\ScheduledDatabaseBackup;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Validate;
@@ -10,6 +11,8 @@ use Livewire\Component;
class CreateScheduledBackup extends Component
{
use AuthorizesRequests;
#[Validate(['required', 'string'])]
public $frequency;
@@ -41,6 +44,8 @@ class CreateScheduledBackup extends Component
public function submit()
{
try {
$this->authorize('manageBackups', $this->database);
$this->validate();
$isValid = validate_cron_expression($this->frequency);

View File

@@ -11,11 +11,14 @@ use App\Models\StandaloneDragonfly;
use App\Support\ValidationPatterns;
use Carbon\Carbon;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class General extends Component
{
use AuthorizesRequests;
public Server $server;
public StandaloneDragonfly $database;
@@ -142,6 +145,8 @@ class General extends Component
public function instantSaveAdvanced()
{
try {
$this->authorize('update', $this->database);
if (! $this->server->isLogDrainEnabled()) {
$this->isLogDrainEnabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -160,6 +165,8 @@ class General extends Component
public function instantSave()
{
try {
$this->authorize('update', $this->database);
if ($this->isPublic && ! $this->publicPort) {
$this->dispatch('error', 'Public port is required.');
$this->isPublic = false;
@@ -197,6 +204,8 @@ class General extends Component
public function submit()
{
try {
$this->authorize('update', $this->database);
if (str($this->publicPort)->isEmpty()) {
$this->publicPort = null;
}
@@ -216,6 +225,8 @@ class General extends Component
public function instantSaveSSL()
{
try {
$this->authorize('update', $this->database);
$this->syncData(true);
$this->dispatch('success', 'SSL configuration updated.');
} catch (Exception $e) {
@@ -226,6 +237,8 @@ class General extends Component
public function regenerateSslCertificate()
{
try {
$this->authorize('update', $this->database);
$existingCert = $this->database->sslCertificates()->first();
if (! $existingCert) {

View File

@@ -7,10 +7,13 @@ use App\Actions\Database\StartDatabase;
use App\Actions\Database\StopDatabase;
use App\Actions\Docker\GetContainersStatus;
use App\Events\ServiceStatusChanged;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
class Heading extends Component
{
use AuthorizesRequests;
public $database;
public array $parameters;
@@ -67,6 +70,8 @@ class Heading extends Component
public function stop()
{
try {
$this->authorize('manage', $this->database);
$this->dispatch('info', 'Gracefully stopping database.');
StopDatabase::dispatch($this->database, false, $this->docker_cleanup);
} catch (\Exception $e) {
@@ -76,12 +81,16 @@ class Heading extends Component
public function restart()
{
$this->authorize('manage', $this->database);
$activity = RestartDatabase::run($this->database);
$this->dispatch('activityMonitor', $activity->id, ServiceStatusChanged::class);
}
public function start()
{
$this->authorize('manage', $this->database);
$activity = StartDatabase::run($this->database);
$this->dispatch('activityMonitor', $activity->id, ServiceStatusChanged::class);
}

View File

@@ -3,12 +3,15 @@
namespace App\Livewire\Project\Database;
use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Livewire\Component;
class Import extends Component
{
use AuthorizesRequests;
public bool $unsupported = false;
public $resource;
@@ -165,6 +168,8 @@ EOD;
public function runImport()
{
$this->authorize('update', $this->resource);
if ($this->filename === '') {
$this->dispatch('error', 'Please select a file to import.');

View File

@@ -11,11 +11,14 @@ use App\Models\StandaloneKeydb;
use App\Support\ValidationPatterns;
use Carbon\Carbon;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class General extends Component
{
use AuthorizesRequests;
public Server $server;
public StandaloneKeydb $database;
@@ -150,6 +153,8 @@ class General extends Component
public function instantSaveAdvanced()
{
try {
$this->authorize('update', $this->database);
if (! $this->server->isLogDrainEnabled()) {
$this->isLogDrainEnabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -168,6 +173,8 @@ class General extends Component
public function instantSave()
{
try {
$this->authorize('update', $this->database);
if ($this->isPublic && ! $this->publicPort) {
$this->dispatch('error', 'Public port is required.');
$this->isPublic = false;
@@ -205,6 +212,8 @@ class General extends Component
public function submit()
{
try {
$this->authorize('manageEnvironment', $this->database);
if (str($this->publicPort)->isEmpty()) {
$this->publicPort = null;
}
@@ -224,6 +233,8 @@ class General extends Component
public function instantSaveSSL()
{
try {
$this->authorize('update', $this->database);
$this->syncData(true);
$this->dispatch('success', 'SSL configuration updated.');
} catch (Exception $e) {
@@ -234,6 +245,8 @@ class General extends Component
public function regenerateSslCertificate()
{
try {
$this->authorize('update', $this->database);
$existingCert = $this->database->sslCertificates()->first();
if (! $existingCert) {

View File

@@ -11,11 +11,14 @@ use App\Models\StandaloneMariadb;
use App\Support\ValidationPatterns;
use Carbon\Carbon;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class General extends Component
{
use AuthorizesRequests;
protected $listeners = ['refresh'];
public Server $server;
@@ -108,6 +111,8 @@ class General extends Component
public function instantSaveAdvanced()
{
try {
$this->authorize('update', $this->database);
if (! $this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -125,6 +130,8 @@ class General extends Component
public function submit()
{
try {
$this->authorize('update', $this->database);
if (str($this->database->public_port)->isEmpty()) {
$this->database->public_port = null;
}
@@ -145,6 +152,8 @@ class General extends Component
public function instantSave()
{
try {
$this->authorize('update', $this->database);
if ($this->database->is_public && ! $this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
@@ -176,6 +185,8 @@ class General extends Component
public function instantSaveSSL()
{
try {
$this->authorize('update', $this->database);
$this->database->save();
$this->dispatch('success', 'SSL configuration updated.');
} catch (Exception $e) {
@@ -186,6 +197,8 @@ class General extends Component
public function regenerateSslCertificate()
{
try {
$this->authorize('update', $this->database);
$existingCert = $this->database->sslCertificates()->first();
if (! $existingCert) {

View File

@@ -11,11 +11,14 @@ use App\Models\StandaloneMongodb;
use App\Support\ValidationPatterns;
use Carbon\Carbon;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class General extends Component
{
use AuthorizesRequests;
protected $listeners = ['refresh'];
public Server $server;
@@ -108,6 +111,8 @@ class General extends Component
public function instantSaveAdvanced()
{
try {
$this->authorize('update', $this->database);
if (! $this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -125,6 +130,8 @@ class General extends Component
public function submit()
{
try {
$this->authorize('update', $this->database);
if (str($this->database->public_port)->isEmpty()) {
$this->database->public_port = null;
}
@@ -148,6 +155,8 @@ class General extends Component
public function instantSave()
{
try {
$this->authorize('update', $this->database);
if ($this->database->is_public && ! $this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
@@ -184,6 +193,8 @@ class General extends Component
public function instantSaveSSL()
{
try {
$this->authorize('update', $this->database);
$this->database->save();
$this->dispatch('success', 'SSL configuration updated.');
} catch (Exception $e) {
@@ -194,6 +205,8 @@ class General extends Component
public function regenerateSslCertificate()
{
try {
$this->authorize('update', $this->database);
$existingCert = $this->database->sslCertificates()->first();
if (! $existingCert) {

View File

@@ -11,11 +11,14 @@ use App\Models\StandaloneMysql;
use App\Support\ValidationPatterns;
use Carbon\Carbon;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class General extends Component
{
use AuthorizesRequests;
protected $listeners = ['refresh'];
public StandaloneMysql $database;
@@ -111,6 +114,8 @@ class General extends Component
public function instantSaveAdvanced()
{
try {
$this->authorize('update', $this->database);
if (! $this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -128,6 +133,8 @@ class General extends Component
public function submit()
{
try {
$this->authorize('update', $this->database);
if (str($this->database->public_port)->isEmpty()) {
$this->database->public_port = null;
}
@@ -148,6 +155,8 @@ class General extends Component
public function instantSave()
{
try {
$this->authorize('update', $this->database);
if ($this->database->is_public && ! $this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
@@ -184,6 +193,8 @@ class General extends Component
public function instantSaveSSL()
{
try {
$this->authorize('update', $this->database);
$this->database->save();
$this->dispatch('success', 'SSL configuration updated.');
} catch (Exception $e) {
@@ -194,6 +205,8 @@ class General extends Component
public function regenerateSslCertificate()
{
try {
$this->authorize('update', $this->database);
$existingCert = $this->database->sslCertificates()->first();
if (! $existingCert) {

View File

@@ -11,11 +11,14 @@ use App\Models\StandalonePostgresql;
use App\Support\ValidationPatterns;
use Carbon\Carbon;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class General extends Component
{
use AuthorizesRequests;
public StandalonePostgresql $database;
public Server $server;
@@ -118,6 +121,8 @@ class General extends Component
public function instantSaveAdvanced()
{
try {
$this->authorize('update', $this->database);
if (! $this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -140,6 +145,8 @@ class General extends Component
public function instantSaveSSL()
{
try {
$this->authorize('update', $this->database);
$this->database->save();
$this->dispatch('success', 'SSL configuration updated.');
$this->db_url = $this->database->internal_db_url;
@@ -152,6 +159,8 @@ class General extends Component
public function regenerateSslCertificate()
{
try {
$this->authorize('update', $this->database);
$existingCert = $this->database->sslCertificates()->first();
if (! $existingCert) {
@@ -184,6 +193,8 @@ class General extends Component
public function instantSave()
{
try {
$this->authorize('update', $this->database);
if ($this->database->is_public && ! $this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
@@ -214,6 +225,8 @@ class General extends Component
public function save_init_script($script)
{
$this->authorize('update', $this->database);
$initScripts = collect($this->database->init_scripts ?? []);
$existingScript = $initScripts->firstWhere('filename', $script['filename']);
@@ -264,6 +277,8 @@ class General extends Component
public function delete_init_script($script)
{
$this->authorize('update', $this->database);
$collection = collect($this->database->init_scripts);
$found = $collection->firstWhere('filename', $script['filename']);
if ($found) {
@@ -298,6 +313,8 @@ class General extends Component
public function save_new_init_script()
{
$this->authorize('update', $this->database);
$this->validate([
'new_filename' => 'required|string',
'new_content' => 'required|string',
@@ -327,6 +344,8 @@ class General extends Component
public function submit()
{
try {
$this->authorize('update', $this->database);
if (str($this->database->public_port)->isEmpty()) {
$this->database->public_port = null;
}

View File

@@ -11,11 +11,14 @@ use App\Models\StandaloneRedis;
use App\Support\ValidationPatterns;
use Carbon\Carbon;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class General extends Component
{
use AuthorizesRequests;
public Server $server;
public StandaloneRedis $database;
@@ -105,6 +108,8 @@ class General extends Component
public function instantSaveAdvanced()
{
try {
$this->authorize('update', $this->database);
if (! $this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -122,6 +127,8 @@ class General extends Component
public function submit()
{
try {
$this->authorize('manageEnvironment', $this->database);
$this->validate();
if (version_compare($this->redis_version, '6.0', '>=')) {
@@ -147,6 +154,8 @@ class General extends Component
public function instantSave()
{
try {
$this->authorize('update', $this->database);
if ($this->database->is_public && ! $this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
@@ -178,6 +187,8 @@ class General extends Component
public function instantSaveSSL()
{
try {
$this->authorize('update', $this->database);
$this->database->save();
$this->dispatch('success', 'SSL configuration updated.');
} catch (Exception $e) {
@@ -188,6 +199,8 @@ class General extends Component
public function regenerateSslCertificate()
{
try {
$this->authorize('update', $this->database);
$existingCert = $this->database->sslCertificates()->first();
if (! $existingCert) {

View File

@@ -3,10 +3,13 @@
namespace App\Livewire\Project\Database;
use App\Models\ScheduledDatabaseBackup;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
class ScheduledBackups extends Component
{
use AuthorizesRequests;
public $database;
public $parameters;
@@ -53,6 +56,8 @@ class ScheduledBackups extends Component
public function setCustomType()
{
$this->authorize('update', $this->database);
$this->database->custom_type = $this->custom_type;
$this->database->save();
$this->dispatch('success', 'Database type set.');
@@ -61,7 +66,10 @@ class ScheduledBackups extends Component
public function delete($scheduled_backup_id): void
{
$this->database->scheduledBackups->find($scheduled_backup_id)->delete();
$backup = $this->database->scheduledBackups->find($scheduled_backup_id);
$this->authorize('manageBackups', $this->database);
$backup->delete();
$this->dispatch('success', 'Scheduled backup deleted.');
$this->refreshScheduledBackups();
}

View File

@@ -45,12 +45,16 @@ class All extends Component
public function instantSave()
{
$this->authorize('manageEnvironment', $this->resource);
try {
$this->authorize('manageEnvironment', $this->resource);
$this->resource->settings->is_env_sorting_enabled = $this->is_env_sorting_enabled;
$this->resource->settings->save();
$this->sortEnvironmentVariables();
$this->dispatch('success', 'Environment variable settings updated.');
$this->resource->settings->is_env_sorting_enabled = $this->is_env_sorting_enabled;
$this->resource->settings->save();
$this->sortEnvironmentVariables();
$this->dispatch('success', 'Environment variable settings updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function sortEnvironmentVariables()
@@ -98,9 +102,8 @@ class All extends Component
public function submit($data = null)
{
$this->authorize('manageEnvironment', $this->resource);
try {
$this->authorize('manageEnvironment', $this->resource);
if ($data === null) {
$this->handleBulkSubmit();
} else {

View File

@@ -0,0 +1,93 @@
<?php
namespace App\Policies;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class DatabasePolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return true;
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, $database): bool
{
return $user->teams()->get()->firstWhere('id', $database->team()->first()->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, $database): Response
{
if ($user->isAdmin() && $user->teams()->get()->firstWhere('id', $database->team()->first()->id) !== null) {
return Response::allow();
}
return Response::deny('As a member, you cannot update this database.<br/><br/>You need at least admin or owner permissions.');
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, $database): bool
{
return $user->isAdmin() && $user->teams()->get()->firstWhere('id', $database->team()->first()->id) !== null;
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, $database): bool
{
return $user->isAdmin() && $user->teams()->get()->firstWhere('id', $database->team()->first()->id) !== null;
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, $database): bool
{
return $user->isAdmin() && $user->teams()->get()->firstWhere('id', $database->team()->first()->id) !== null;
}
/**
* Determine whether the user can start/stop the database.
*/
public function manage(User $user, $database): bool
{
return $user->isAdmin() && $user->teams()->get()->firstWhere('id', $database->team()->first()->id) !== null;
}
/**
* Determine whether the user can manage database backups.
*/
public function manageBackups(User $user, $database): bool
{
return $user->isAdmin() && $user->teams()->get()->firstWhere('id', $database->team()->first()->id) !== null;
}
/**
* Determine whether the user can manage environment variables.
*/
public function manageEnvironment(User $user, $database): bool
{
return $user->isAdmin() && $user->teams()->get()->firstWhere('id', $database->team()->first()->id) !== null;
}
}