From a3e78a36778e98634e6ad37ebdbef494aeb6b1bc Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 18 Aug 2026 10:18:52 +0200 Subject: [PATCH] fix(authorization): gate mutable Livewire controls by policy Disable unauthorized listbox inputs and hide application deploy actions, with updated disabled styling and coverage. --- resources/css/app.css | 8 +- .../components/database-status-info.blade.php | 4 +- .../views/components/forms/listbox.blade.php | 7 ++ .../livewire/notifications/discord.blade.php | 2 +- .../livewire/notifications/email.blade.php | 10 +-- .../project/application/domains.blade.php | 4 +- .../project/application/heading.blade.php | 4 + .../project/application/swarm.blade.php | 2 +- .../database/clickhouse/general.blade.php | 4 +- .../database/dragonfly/general.blade.php | 4 +- .../project/database/heading.blade.php | 4 + .../project/database/keydb/general.blade.php | 4 +- .../database/mariadb/general.blade.php | 4 +- .../database/mongodb/general.blade.php | 4 +- .../project/database/mysql/general.blade.php | 4 +- .../database/postgresql/general.blade.php | 4 +- .../project/database/redis/general.blade.php | 4 +- .../project/service/domains.blade.php | 4 +- .../project/service/heading.blade.php | 4 + .../project/service/stack-form.blade.php | 2 +- .../shared/scheduled-task/show.blade.php | 10 +-- .../storages/volume-backups/s3.blade.php | 4 +- .../livewire/server/log-drains.blade.php | 6 +- .../views/livewire/server/swarm.blade.php | 4 +- .../livewire/source/github/change.blade.php | 4 +- .../livewire/source/gitlab/change.blade.php | 2 +- resources/views/livewire/team/index.blade.php | 2 +- .../Feature/ListboxTriggerTruncationTest.php | 9 ++ ...bleLivewireComponentsAuthorizationTest.php | 83 +++++++++++++++++++ 29 files changed, 164 insertions(+), 47 deletions(-) diff --git a/resources/css/app.css b/resources/css/app.css index 95e0207ce2..636cf2ce8a 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -1905,7 +1905,13 @@ html[data-theme="custom"] textarea:disabled { .listbox-trigger:disabled { cursor: not-allowed; - opacity: 0.5; + background-color: var(--color-neutral-100); + color: var(--color-neutral-400); +} + +.dark .listbox-trigger:disabled { + background-color: color-mix(in oklab, var(--color-white) 3%, transparent); + color: var(--color-fg-faint); } .listbox-trigger:focus-visible { diff --git a/resources/views/components/database-status-info.blade.php b/resources/views/components/database-status-info.blade.php index 5e352c206b..b9298e2689 100644 --- a/resources/views/components/database-status-info.blade.php +++ b/resources/views/components/database-status-info.blade.php @@ -65,7 +65,7 @@ @endif
- @if ($sslModeOptions) - - @if (isCloud())
- -
- -
- - @if ($isCompose && count($composeServices) > 0) - @if (!($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw))) + @can('deploy', $application)
@@ -149,6 +150,7 @@ @endif
+ @endcan @endif + @can('deploy', $application)
@@ -279,6 +282,7 @@ @endif
+ @endcan @endif
diff --git a/resources/views/livewire/project/application/swarm.blade.php b/resources/views/livewire/project/application/swarm.blade.php index fc8112afbe..6bd593b933 100644 --- a/resources/views/livewire/project/application/swarm.blade.php +++ b/resources/views/livewire/project/application/swarm.blade.php @@ -14,7 +14,7 @@
-
user()->can('update', $database)" canGate="update" :canResource="$database" :options="[ ['value' => false, 'label' => 'Private'], ['value' => true, 'label' => blank($publicPort) ? 'Public through TCP proxy (set public port first)' : 'Public through TCP proxy', 'disabled' => blank($publicPort)], ]" /> @@ -94,7 +94,7 @@ -
user()->can('update', $database)" canGate="update" :canResource="$database" :options="[ ['value' => false, 'label' => 'Private'], ['value' => true, 'label' => blank($publicPort) ? 'Public through TCP proxy (set public port first)' : 'Public through TCP proxy', 'disabled' => blank($publicPort)], ]" /> @@ -95,7 +95,7 @@ - @if ($database->destination->server->isFunctional()) + @can('manage', $database)
@@ -127,6 +128,7 @@ @endif
+ @endcan @endif
@@ -137,6 +139,7 @@ class="resource-heading-navbar application-heading-actions flex w-auto min-w-0 items-center justify-end gap-1 overflow-visible">
@if ($database->destination->server->isFunctional()) + @can('manage', $database)
@if (! $databaseStatus->startsWith('exited'))
+ @endcan @else @endif diff --git a/resources/views/livewire/project/database/keydb/general.blade.php b/resources/views/livewire/project/database/keydb/general.blade.php index e6b7ff985f..6d894df04b 100644 --- a/resources/views/livewire/project/database/keydb/general.blade.php +++ b/resources/views/livewire/project/database/keydb/general.blade.php @@ -81,7 +81,7 @@
user()->can('update', $database)" canGate="update" :canResource="$database" :options="[ ['value' => false, 'label' => 'Private'], ['value' => true, 'label' => blank($publicPort) ? 'Public through TCP proxy (set public port first)' : 'Public through TCP proxy', 'disabled' => blank($publicPort)], ]" /> @@ -104,7 +104,7 @@ -
user()->can('update', $database)" canGate="update" :canResource="$database" :options="[ ['value' => false, 'label' => 'Private'], ['value' => true, 'label' => blank($publicPort) ? 'Public through TCP proxy (set public port first)' : 'Public through TCP proxy', 'disabled' => blank($publicPort)], ]" /> @@ -107,7 +107,7 @@ -
user()->can('update', $database)" canGate="update" :canResource="$database" :options="[ ['value' => false, 'label' => 'Private'], ['value' => true, 'label' => blank($publicPort) ? 'Public through TCP proxy (set public port first)' : 'Public through TCP proxy', 'disabled' => blank($publicPort)], ]" /> @@ -104,7 +104,7 @@ -
user()->can('update', $database)" canGate="update" :canResource="$database" :options="[ ['value' => false, 'label' => 'Private'], ['value' => true, 'label' => blank($publicPort) ? 'Public through TCP proxy (set public port first)' : 'Public through TCP proxy', 'disabled' => blank($publicPort)], ]" /> @@ -107,7 +107,7 @@ -
user()->can('update', $database)" canGate="update" :canResource="$database" :options="[ ['value' => false, 'label' => 'Private'], ['value' => true, 'label' => blank($publicPort) ? 'Public through TCP proxy (set public port first)' : 'Public through TCP proxy', 'disabled' => blank($publicPort)], ]" /> @@ -111,7 +111,7 @@ -
user()->can('update', $database)" canGate="update" :canResource="$database" :options="[ ['value' => false, 'label' => 'Private'], ['value' => true, 'label' => blank($publicPort) ? 'Public through TCP proxy (set public port first)' : 'Public through TCP proxy', 'disabled' => blank($publicPort)], ]" /> @@ -110,7 +110,7 @@ - {{-- Always show which service receives the domain --}} - {{ $heading }} @if ($hasHttpsDomains)
- @if ($service->isDeployable) + @can('deploy', $service)
@@ -165,6 +166,7 @@ @endif
+ @endcan @else @@ -183,6 +185,7 @@
+ @can('deploy', $service)
@@ -247,6 +250,7 @@ @endif
+ @endcan @else diff --git a/resources/views/livewire/project/service/stack-form.blade.php b/resources/views/livewire/project/service/stack-form.blade.php index d6c55ecfe3..423ea0aaf8 100644 --- a/resources/views/livewire/project/service/stack-form.blade.php +++ b/resources/views/livewire/project/service/stack-form.blade.php @@ -52,7 +52,7 @@ - - - - -
-
diff --git a/resources/views/livewire/project/shared/storages/volume-backups/s3.blade.php b/resources/views/livewire/project/shared/storages/volume-backups/s3.blade.php index 2dc32b3e31..eeaca78479 100644 --- a/resources/views/livewire/project/shared/storages/volume-backups/s3.blade.php +++ b/resources/views/livewire/project/shared/storages/volume-backups/s3.blade.php @@ -39,13 +39,13 @@ @endif
- -
-
-
- - - - -
diff --git a/resources/views/livewire/source/gitlab/change.blade.php b/resources/views/livewire/source/gitlab/change.blade.php index a8a499e6ba..5629e1e364 100644 --- a/resources/views/livewire/source/gitlab/change.blade.php +++ b/resources/views/livewire/source/gitlab/change.blade.php @@ -82,7 +82,7 @@
-
- false); + + $html = Blade::render(<<<'BLADE' + + BLADE); + + expect($html)->toMatch('/]*id="status-trigger"[^>]*\sdisabled(?:[=\s>])/'); +}); + +it('declares gate attributes on form controls with update permission checks', function () { + $controlPattern = '/|<\/x-forms\.[^>]+>)/s'; + + foreach (File::allFiles(resource_path('views')) as $file) { + $path = $file->getPathname(); + $source = file_get_contents($path); + preg_match_all($controlPattern, $source, $controls); + + foreach ($controls[0] as $control) { + if (! preg_match('/can\(\s*[\'\"]update[\'\"]/', $control)) { + continue; + } + + expect($control, $path)->toContain('canGate="update"') + ->toContain(':canResource='); + } + } +}); + +it('hides resource action menus when the user cannot manage the resource', function (string $path, string $ability, string $resource, string $prefix) { + $source = file_get_contents(resource_path($path)); + + foreach (['mobile', 'desktop'] as $viewport) { + expect($source)->toMatch( + "/@can\\('{$ability}', \\$".$resource."\\)[\\s\\S]*?
with([ + 'application actions' => ['views/livewire/project/application/heading.blade.php', 'deploy', 'application', 'application'], + 'service actions' => ['views/livewire/project/service/heading.blade.php', 'deploy', 'service', 'service'], + 'database actions' => ['views/livewire/project/database/heading.blade.php', 'manage', 'database', 'database'], + 'server actions' => ['views/livewire/server/navbar.blade.php', 'manageProxy', 'server', 'server'], +]); + it('keeps mutable Livewire components behind authorization checks', function (string $path, array $requiredNeedles) { $source = file_get_contents(base_path($path)); @@ -69,4 +118,38 @@ it('authorizes every volume backup form control', function (string $path, array '/wire:click="checkAllDns"/s', ], ], + 'postgres public access control' => [ + 'resources/views/livewire/project/database/postgresql/general.blade.php', + [ + '/ [ + 'resources/views/livewire/project/database/redis/general.blade.php', + ['/ [ + 'resources/views/livewire/project/database/mongodb/general.blade.php', + ['/ [ + 'resources/views/livewire/project/database/clickhouse/general.blade.php', + ['/ [ + 'resources/views/livewire/project/database/mariadb/general.blade.php', + ['/ [ + 'resources/views/livewire/project/database/dragonfly/general.blade.php', + ['/ [ + 'resources/views/livewire/project/database/mysql/general.blade.php', + ['/ [ + 'resources/views/livewire/project/database/keydb/general.blade.php', + ['/