feat(auth): enhance authorization checks in Livewire components for resource management

This commit is contained in:
Andras Bacsai
2025-08-24 17:14:55 +02:00
parent ae79a98d72
commit ae1b0de561
13 changed files with 184 additions and 77 deletions

View File

@@ -2,6 +2,7 @@
namespace App\Livewire; namespace App\Livewire;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationDeploymentQueue;
use App\Models\PrivateKey; use App\Models\PrivateKey;
use App\Models\Project; use App\Models\Project;
@@ -30,6 +31,12 @@ class Dashboard extends Component
public function cleanupQueue() public function cleanupQueue()
{ {
try {
$this->authorize('cleanupDeploymentQueue', Application::class);
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
return handleError($e, $this);
}
Artisan::queue('cleanup:deployment-queue', [ Artisan::queue('cleanup:deployment-queue', [
'--team-id' => currentTeam()->id, '--team-id' => currentTeam()->id,
]); ]);

View File

@@ -3,10 +3,13 @@
namespace App\Livewire\Project; namespace App\Livewire\Project;
use App\Models\Environment; use App\Models\Environment;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component; use Livewire\Component;
class DeleteEnvironment extends Component class DeleteEnvironment extends Component
{ {
use AuthorizesRequests;
public int $environment_id; public int $environment_id;
public bool $disabled = false; public bool $disabled = false;
@@ -31,6 +34,8 @@ class DeleteEnvironment extends Component
'environment_id' => 'required|int', 'environment_id' => 'required|int',
]); ]);
$environment = Environment::findOrFail($this->environment_id); $environment = Environment::findOrFail($this->environment_id);
$this->authorize('delete', $environment);
if ($environment->isEmpty()) { if ($environment->isEmpty()) {
$environment->delete(); $environment->delete();

View File

@@ -3,10 +3,13 @@
namespace App\Livewire\Project; namespace App\Livewire\Project;
use App\Models\Project; use App\Models\Project;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component; use Livewire\Component;
class DeleteProject extends Component class DeleteProject extends Component
{ {
use AuthorizesRequests;
public array $parameters; public array $parameters;
public int $project_id; public int $project_id;
@@ -27,6 +30,8 @@ class DeleteProject extends Component
'project_id' => 'required|int', 'project_id' => 'required|int',
]); ]);
$project = Project::findOrFail($this->project_id); $project = Project::findOrFail($this->project_id);
$this->authorize('delete', $project);
if ($project->isEmpty()) { if ($project->isEmpty()) {
$project->delete(); $project->delete();

View File

@@ -2,41 +2,19 @@
namespace App\Livewire\Project\Resource; namespace App\Livewire\Project\Resource;
use App\Models\Application;
use App\Models\EnvironmentVariable; use App\Models\EnvironmentVariable;
use App\Models\Service; use App\Models\Service;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDocker; use App\Models\StandaloneDocker;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component; use Livewire\Component;
class Create extends Component class Create extends Component
{ {
use AuthorizesRequests;
public $type; public $type;
public $project; public $project;
public function mount() public function mount()
{ {
$this->authorize('create', StandalonePostgresql::class);
$this->authorize('create', StandaloneRedis::class);
$this->authorize('create', StandaloneMongodb::class);
$this->authorize('create', StandaloneMysql::class);
$this->authorize('create', StandaloneMariadb::class);
$this->authorize('create', StandaloneKeydb::class);
$this->authorize('create', StandaloneDragonfly::class);
$this->authorize('create', StandaloneClickhouse::class);
$this->authorize('create', Service::class);
$this->authorize('create', Application::class);
$type = str(request()->query('type')); $type = str(request()->query('type'));
$destination_uuid = request()->query('destination'); $destination_uuid = request()->query('destination');
@@ -57,32 +35,24 @@ class Create extends Component
if (in_array($type, DATABASE_TYPES)) { if (in_array($type, DATABASE_TYPES)) {
if ($type->value() === 'postgresql') { if ($type->value() === 'postgresql') {
$this->authorize('create', StandalonePostgresql::class);
$database = create_standalone_postgresql( $database = create_standalone_postgresql(
environmentId: $environment->id, environmentId: $environment->id,
destinationUuid: $destination_uuid, destinationUuid: $destination_uuid,
databaseImage: $database_image databaseImage: $database_image
); );
} elseif ($type->value() === 'redis') { } elseif ($type->value() === 'redis') {
$this->authorize('create', StandaloneRedis::class);
$database = create_standalone_redis($environment->id, $destination_uuid); $database = create_standalone_redis($environment->id, $destination_uuid);
} elseif ($type->value() === 'mongodb') { } elseif ($type->value() === 'mongodb') {
$this->authorize('create', StandaloneMongodb::class);
$database = create_standalone_mongodb($environment->id, $destination_uuid); $database = create_standalone_mongodb($environment->id, $destination_uuid);
} elseif ($type->value() === 'mysql') { } elseif ($type->value() === 'mysql') {
$this->authorize('create', StandaloneMysql::class);
$database = create_standalone_mysql($environment->id, $destination_uuid); $database = create_standalone_mysql($environment->id, $destination_uuid);
} elseif ($type->value() === 'mariadb') { } elseif ($type->value() === 'mariadb') {
$this->authorize('create', StandaloneMariadb::class);
$database = create_standalone_mariadb($environment->id, $destination_uuid); $database = create_standalone_mariadb($environment->id, $destination_uuid);
} elseif ($type->value() === 'keydb') { } elseif ($type->value() === 'keydb') {
$this->authorize('create', StandaloneKeydb::class);
$database = create_standalone_keydb($environment->id, $destination_uuid); $database = create_standalone_keydb($environment->id, $destination_uuid);
} elseif ($type->value() === 'dragonfly') { } elseif ($type->value() === 'dragonfly') {
$this->authorize('create', StandaloneDragonfly::class);
$database = create_standalone_dragonfly($environment->id, $destination_uuid); $database = create_standalone_dragonfly($environment->id, $destination_uuid);
} elseif ($type->value() === 'clickhouse') { } elseif ($type->value() === 'clickhouse') {
$this->authorize('create', StandaloneClickhouse::class);
$database = create_standalone_clickhouse($environment->id, $destination_uuid); $database = create_standalone_clickhouse($environment->id, $destination_uuid);
} }

View File

@@ -37,6 +37,8 @@ class Danger extends Component
public string $resourceDomain = ''; public string $resourceDomain = '';
public bool $canDelete = false;
public function mount() public function mount()
{ {
$parameters = get_route_parameters(); $parameters = get_route_parameters();
@@ -80,6 +82,13 @@ class Danger extends Component
'service-database' => $this->resource->name ?? 'Service Database', 'service-database' => $this->resource->name ?? 'Service Database',
default => 'Unknown Resource', default => 'Unknown Resource',
}; };
// Check if user can delete this resource
try {
$this->canDelete = auth()->user()->can('delete', $this->resource);
} catch (\Exception $e) {
$this->canDelete = false;
}
} }
public function delete($password) public function delete($password)

View File

@@ -30,18 +30,22 @@
</div> </div>
<div class="flex items-center justify-center gap-2 text-xs font-bold"> <div class="flex items-center justify-center gap-2 text-xs font-bold">
@if ($project->environments->first()) @if ($project->environments->first())
<a class="hover:underline" wire:click.stop @can('createAnyResource')
href="{{ route('project.resource.create', [ <a class="hover:underline" wire:click.stop
'project_uuid' => $project->uuid, href="{{ route('project.resource.create', [
'environment_uuid' => $project->environments->first()->uuid, 'project_uuid' => $project->uuid,
]) }}"> 'environment_uuid' => $project->environments->first()->uuid,
<span class="p-2 font-bold">+ Add Resource</span> ]) }}">
</a> <span class="p-2 font-bold">+ Add Resource</span>
</a>
@endcan
@endif @endif
<a class="hover:underline" wire:click.stop @can('update', $project)
href="{{ route('project.edit', ['project_uuid' => $project->uuid]) }}"> <a class="hover:underline" wire:click.stop
Settings href="{{ route('project.edit', ['project_uuid' => $project->uuid]) }}">
</a> Settings
</a>
@endcan
</div> </div>
</div> </div>
</div> </div>
@@ -129,10 +133,12 @@
@if (count($deploymentsPerServer) > 0) @if (count($deploymentsPerServer) > 0)
<x-loading /> <x-loading />
@endif @endif
<x-modal-confirmation title="Confirm Cleanup Queues?" buttonTitle="Cleanup Queues" isErrorButton @can('cleanupDeploymentQueue', Application::class)
submitAction="cleanupQueue" :actions="['All running Deployment Queues will be cleaned up.']" :confirmWithText="false" :confirmWithPassword="false" <x-modal-confirmation title="Confirm Cleanup Queues?" buttonTitle="Cleanup Queues" isErrorButton
step2ButtonText="Permanently Cleanup Deployment Queues" :dispatchEvent="true" submitAction="cleanupQueue" :actions="['All running Deployment Queues will be cleaned up.']" :confirmWithText="false" :confirmWithPassword="false"
dispatchEventType="success" dispatchEventMessage="Deployment Queues cleanup started." /> step2ButtonText="Permanently Cleanup Deployment Queues" :dispatchEvent="true"
dispatchEventType="success" dispatchEventMessage="Deployment Queues cleanup started." />
@endcan
</div> </div>
<div wire:poll.3000ms="loadDeployments" class="grid grid-cols-1"> <div wire:poll.3000ms="loadDeployments" class="grid grid-cols-1">
@forelse ($deploymentsPerServer as $serverName => $deployments) @forelse ($deploymentsPerServer as $serverName => $deployments)

View File

@@ -50,7 +50,16 @@
<x-forms.input <x-forms.input
helper="You can add a custom name for your container.<br><br>The name will be converted to slug format when you save it. <span class='font-bold dark:text-warning'>You will lose the rolling update feature!</span>" helper="You can add a custom name for your container.<br><br>The name will be converted to slug format when you save it. <span class='font-bold dark:text-warning'>You will lose the rolling update feature!</span>"
instantSave id="customInternalName" label="Custom Container Name" /> instantSave id="customInternalName" label="Custom Container Name" />
<x-forms.button type="submit">Save</x-forms.button> @can('update', $application)
<x-forms.button type="submit">
Save
</x-forms.button>
@else
<x-forms.button type="submit" disabled
title="You don't have permission to update this application. Contact your team administrator for access.">
Save
</x-forms.button>
@endcan
</form> </form>
@endif @endif
@if ($application->build_pack === 'dockercompose') @if ($application->build_pack === 'dockercompose')
@@ -78,7 +87,16 @@
<div class="flex gap-2 items-end pt-4"> <div class="flex gap-2 items-end pt-4">
<h3>GPU</h3> <h3>GPU</h3>
@if ($isGpuEnabled) @if ($isGpuEnabled)
<x-forms.button type="submit">Save</x-forms.button> @can('update', $application)
<x-forms.button type="submit">
Save
</x-forms.button>
@else
<x-forms.button type="submit" disabled
title="You don't have permission to update this application. Contact your team administrator for access.">
Save
</x-forms.button>
@endcan
@endif @endif
</div> </div>
@endif @endif

View File

@@ -6,6 +6,11 @@
<x-forms.button type="submit"> <x-forms.button type="submit">
Save Save
</x-forms.button> </x-forms.button>
@else
<x-forms.button type="submit" disabled
title="You don't have permission to update this application. Contact your team administrator for access.">
Save
</x-forms.button>
@endcan @endcan
{{-- <x-forms.button wire:click="downloadConfig"> {{-- <x-forms.button wire:click="downloadConfig">
@@ -82,8 +87,14 @@
placeholder="Empty means default configuration will be used." label="Custom Nginx Configuration" placeholder="Empty means default configuration will be used." label="Custom Nginx Configuration"
helper="You can add custom Nginx configuration here." /> helper="You can add custom Nginx configuration here." />
@can('update', $application) @can('update', $application)
<x-forms.button wire:click="generateNginxConfiguration">Generate Default Nginx <x-forms.button wire:click="generateNginxConfiguration">
Configuration</x-forms.button> Generate Default Nginx Configuration
</x-forms.button>
@else
<x-forms.button wire:click="generateNginxConfiguration" disabled
title="You don't have permission to update this application. Contact your team administrator for access.">
Generate Default Nginx Configuration
</x-forms.button>
@endcan @endcan
@endif @endif
<div class="w-96 pb-6"> <div class="w-96 pb-6">

View File

@@ -2,9 +2,16 @@
<form wire:submit='submit' class="flex flex-col"> <form wire:submit='submit' class="flex flex-col">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h2>Swarm Configuration</h2> <h2>Swarm Configuration</h2>
<x-forms.button type="submit"> @can('update', $application)
Save <x-forms.button type="submit">
</x-forms.button> Save
</x-forms.button>
@else
<x-forms.button type="submit" disabled
title="You don't have permission to update this application. Contact your team administrator for access.">
Save
</x-forms.button>
@endcan
</div> </div>
<div class="flex flex-col gap-2 py-4"> <div class="flex flex-col gap-2 py-4">
<div class="flex flex-col items-end gap-2 xl:flex-row"> <div class="flex flex-col items-end gap-2 xl:flex-row">

View File

@@ -5,8 +5,19 @@
<form wire:submit='submit' class="flex flex-col"> <form wire:submit='submit' class="flex flex-col">
<div class="flex items-end gap-2"> <div class="flex items-end gap-2">
<h1>Environment: {{ data_get_str($environment, 'name')->limit(15) }}</h1> <h1>Environment: {{ data_get_str($environment, 'name')->limit(15) }}</h1>
<x-forms.button type="submit">Save</x-forms.button> @can('update', $environment)
<livewire:project.delete-environment :disabled="!$environment->isEmpty()" :environment_id="$environment->id" /> <x-forms.button type="submit">
Save
</x-forms.button>
@else
<x-forms.button type="submit" disabled
title="You don't have permission to update this environment. Contact your team administrator for access.">
Save
</x-forms.button>
@endcan
@can('delete', $environment)
<livewire:project.delete-environment :disabled="!$environment->isEmpty()" :environment_id="$environment->id" />
@endcan
</div> </div>
<nav class="flex pt-2 pb-10"> <nav class="flex pt-2 pb-10">
<ol class="flex flex-wrap items-center gap-y-1"> <ol class="flex flex-wrap items-center gap-y-1">

View File

@@ -6,20 +6,28 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h1>Resources</h1> <h1>Resources</h1>
@if ($environment->isEmpty()) @if ($environment->isEmpty())
<a class="button" @can('createAnyResource')
href="{{ route('project.clone-me', ['project_uuid' => data_get($project, 'uuid'), 'environment_uuid' => data_get($environment, 'uuid')]) }}"> <a class="button"
Clone href="{{ route('project.clone-me', ['project_uuid' => data_get($project, 'uuid'), 'environment_uuid' => data_get($environment, 'uuid')]) }}">
</a> Clone
</a>
@endcan
@else @else
<a href="{{ route('project.resource.create', ['project_uuid' => data_get($parameters, 'project_uuid'), 'environment_uuid' => data_get($environment, 'uuid')]) }}" @can('createAnyResource')
class="button">+ <a href="{{ route('project.resource.create', ['project_uuid' => data_get($parameters, 'project_uuid'), 'environment_uuid' => data_get($environment, 'uuid')]) }}"
New</a> class="button">+
<a class="button" New</a>
href="{{ route('project.clone-me', ['project_uuid' => data_get($project, 'uuid'), 'environment_uuid' => data_get($environment, 'uuid')]) }}"> @endcan
Clone @can('createAnyResource')
</a> <a class="button"
href="{{ route('project.clone-me', ['project_uuid' => data_get($project, 'uuid'), 'environment_uuid' => data_get($environment, 'uuid')]) }}">
Clone
</a>
@endcan
@endif @endif
<livewire:project.delete-environment :disabled="!$environment->isEmpty()" :environment_id="$environment->id" /> @can('delete', $environment)
<livewire:project.delete-environment :disabled="!$environment->isEmpty()" :environment_id="$environment->id" />
@endcan
</div> </div>
<nav class="flex pt-2 pb-6"> <nav class="flex pt-2 pb-6">
<ol class="flex items-center"> <ol class="flex items-center">
@@ -44,14 +52,39 @@
</nav> </nav>
</div> </div>
@if ($environment->isEmpty()) @if ($environment->isEmpty())
<a href="{{ route('project.resource.create', ['project_uuid' => data_get($parameters, 'project_uuid'), 'environment_uuid' => data_get($environment, 'uuid')]) }}" @can('createAnyResource')
class="items-center justify-center box">+ Add New Resource</a> <a href="{{ route('project.resource.create', ['project_uuid' => data_get($parameters, 'project_uuid'), 'environment_uuid' => data_get($environment, 'uuid')]) }}"
class="items-center justify-center box">+ Add New Resource</a>
@else
<div
class="flex flex-col items-center justify-center p-8 text-center border border-dashed border-neutral-300 dark:border-coolgray-300 rounded-lg">
<h3 class="mb-2 text-lg font-semibold text-neutral-600 dark:text-neutral-400">No Resources Found</h3>
<p class="text-sm text-neutral-600 dark:text-neutral-400">
This environment doesn't have any resources yet.<br>
Contact your team administrator to add resources.
</p>
</div>
@endcan
@else @else
<div x-data="searchComponent()"> <div x-data="searchComponent()">
<x-forms.input placeholder="Search for name, fqdn..." x-model="search" id="null" /> <x-forms.input placeholder="Search for name, fqdn..." x-model="search" id="null" />
<template <template
x-if="filteredApplications.length === 0 && filteredDatabases.length === 0 && filteredServices.length === 0"> x-if="filteredApplications.length === 0 && filteredDatabases.length === 0 && filteredServices.length === 0">
<div>No resource found with the search term "<span x-text="search"></span>".</div> <div class="flex flex-col items-center justify-center p-8 text-center">
<div x-show="search.length > 0">
<p class="text-neutral-600 dark:text-neutral-400">No resource found with the search term "<span
class="font-semibold" x-text="search"></span>".</p>
<p class="text-sm text-neutral-500 dark:text-neutral-500 mt-1">Try adjusting your search
criteria.</p>
</div>
<div x-show="search.length === 0">
<p class="text-neutral-600 dark:text-neutral-400">No resources found in this environment.</p>
@cannot('createAnyResource')
<p class="text-sm text-neutral-500 dark:text-neutral-500 mt-1">Contact your team administrator
to add resources.</p>
@endcannot
</div>
</div>
</template> </template>
<template x-if="filteredApplications.length > 0"> <template x-if="filteredApplications.length > 0">

View File

@@ -4,8 +4,24 @@
<h4 class="pt-4">Delete Resource</h4> <h4 class="pt-4">Delete Resource</h4>
<div class="pb-4">This will stop your containers, delete all related data, etc. Beware! There is no coming back! <div class="pb-4">This will stop your containers, delete all related data, etc. Beware! There is no coming back!
</div> </div>
<x-modal-confirmation title="Confirm Resource Deletion?" buttonTitle="Delete" isErrorButton submitAction="delete"
buttonTitle="Delete" :checkboxes="$checkboxes" :actions="['Permanently delete all containers of this resource.']" confirmationText="{{ $resourceName }}" @if ($canDelete)
confirmationLabel="Please confirm the execution of the actions by entering the Resource Name below" <x-modal-confirmation title="Confirm Resource Deletion?" buttonTitle="Delete" isErrorButton submitAction="delete"
shortConfirmationLabel="Resource Name" /> buttonTitle="Delete" :checkboxes="$checkboxes" :actions="['Permanently delete all containers of this resource.']" confirmationText="{{ $resourceName }}"
confirmationLabel="Please confirm the execution of the actions by entering the Resource Name below"
shortConfirmationLabel="Resource Name" />
@else
<div class="flex items-center gap-2 p-4 border border-red-500 rounded-lg bg-red-50 dark:bg-red-900/20">
<svg class="w-5 h-5 text-red-500" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z"
clip-rule="evenodd"></path>
</svg>
<div>
<div class="font-semibold text-red-700 dark:text-red-300">Insufficient Permissions</div>
<div class="text-sm text-red-600 dark:text-red-400">You don't have permission to delete this resource.
Contact your team administrator for access.</div>
</div>
</div>
@endif
</div> </div>

View File

@@ -7,12 +7,21 @@
<x-modal-input buttonTitle="+ Add" title="New Environment"> <x-modal-input buttonTitle="+ Add" title="New Environment">
<form class="flex flex-col w-full gap-2 rounded-sm" wire:submit='submit'> <form class="flex flex-col w-full gap-2 rounded-sm" wire:submit='submit'>
<x-forms.input placeholder="production" id="name" label="Name" required /> <x-forms.input placeholder="production" id="name" label="Name" required />
<x-forms.button type="submit"> @can('update', $project)
Save <x-forms.button type="submit">
</x-forms.button> Save
</x-forms.button>
@else
<x-forms.button type="submit" disabled
title="You don't have permission to update this project. Contact your team administrator for access.">
Save
</x-forms.button>
@endcan
</form> </form>
</x-modal-input> </x-modal-input>
<livewire:project.delete-project :disabled="!$project->isEmpty()" :project_id="$project->id" /> @can('delete', $project)
<livewire:project.delete-project :disabled="!$project->isEmpty()" :project_id="$project->id" />
@endcan
</div> </div>
<div class="text-xs truncate subtitle lg:text-sm">{{ $project->name }}.</div> <div class="text-xs truncate subtitle lg:text-sm">{{ $project->name }}.</div>
<div class="grid gap-2 lg:grid-cols-2"> <div class="grid gap-2 lg:grid-cols-2">