mirror of
https://github.com/coollabsio/coolify.git
synced 2026-08-24 02:24:11 -05:00
fix(authorization): gate mutable Livewire controls by policy
Disable unauthorized listbox inputs and hide application deploy actions, with updated disabled styling and coverage.
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
</div>
|
||||
@endif
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<x-forms.listbox id="enableSsl" label="SSL"
|
||||
<x-forms.listbox canGate="update" :canResource="$database" id="enableSsl" label="SSL"
|
||||
onChange="instantSaveSSL"
|
||||
:disabled="! $isExited || ! auth()->user()?->can('update', $database)"
|
||||
:options="[
|
||||
@@ -73,7 +73,7 @@
|
||||
['value' => false, 'label' => 'Disabled'],
|
||||
]" />
|
||||
@if ($sslModeOptions)
|
||||
<x-forms.listbox id="sslMode" label="SSL mode" :helper="$sslModeHelper"
|
||||
<x-forms.listbox canGate="update" :canResource="$database" id="sslMode" label="SSL mode" :helper="$sslModeHelper"
|
||||
onChange="instantSaveSSL"
|
||||
:disabled="! $enableSsl || ! $isExited || ! auth()->user()?->can('update', $database)"
|
||||
:options="collect($sslModeOptions)->map(fn ($option, $value) => [
|
||||
|
||||
@@ -16,9 +16,16 @@
|
||||
'tooltip' => true,
|
||||
'portal' => false,
|
||||
'preserveValue' => false,
|
||||
'canGate' => null,
|
||||
'canResource' => null,
|
||||
'autoDisable' => true,
|
||||
])
|
||||
|
||||
@php
|
||||
if ($canGate && $canResource && $autoDisable && ! Illuminate\Support\Facades\Gate::allows($canGate, $canResource)) {
|
||||
$disabled = true;
|
||||
}
|
||||
|
||||
$triggerId = ($htmlId ?? $id).'-trigger';
|
||||
$panelId = ($htmlId ?? $id).'-panel';
|
||||
@endphp
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</x-slot:actions>
|
||||
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<x-forms.listbox id="discordPingEnabled" label="Critical event mention"
|
||||
<x-forms.listbox canGate="update" :canResource="$settings" id="discordPingEnabled" label="Critical event mention"
|
||||
helper="Mention @here when a critical event occurs."
|
||||
onChange="instantSaveDiscordPingEnabled"
|
||||
:disabled="!auth()->user()->can('update', $settings)" :options="[
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<div class="lg:col-span-2">
|
||||
@if (isCloud())
|
||||
<div class="w-full sm:w-72">
|
||||
<x-forms.listbox id="useInstanceEmailSettings" label="Email service"
|
||||
<x-forms.listbox canGate="update" :canResource="$settings" id="useInstanceEmailSettings" label="Email service"
|
||||
onChange="instantSave"
|
||||
:disabled="!auth()->user()->can('update', $settings)" :options="[
|
||||
['value' => true, 'label' => 'Use hosted email service'],
|
||||
@@ -50,7 +50,7 @@
|
||||
</div>
|
||||
@else
|
||||
<div class="w-full sm:w-72">
|
||||
<x-forms.listbox id="useInstanceEmailSettings" label="Email service"
|
||||
<x-forms.listbox canGate="update" :canResource="$settings" id="useInstanceEmailSettings" label="Email service"
|
||||
onChange="instantSave"
|
||||
:disabled="!auth()->user()->can('update', $settings)" :options="[
|
||||
['value' => true, 'label' => 'Use system-wide settings'],
|
||||
@@ -85,7 +85,7 @@
|
||||
<div class="grid gap-4 lg:grid-cols-3">
|
||||
<div class="lg:col-span-3">
|
||||
<div class="w-full sm:w-72">
|
||||
<x-forms.listbox id="smtpEnabled" label="SMTP delivery"
|
||||
<x-forms.listbox canGate="update" :canResource="$settings" id="smtpEnabled" label="SMTP delivery"
|
||||
onChange="submitSmtp"
|
||||
:disabled="!auth()->user()->can('update', $settings)" :options="[
|
||||
['value' => true, 'label' => 'Enabled'],
|
||||
@@ -97,7 +97,7 @@
|
||||
placeholder="smtp.mailgun.org" label="Host" />
|
||||
<x-forms.input canGate="update" :canResource="$settings" required id="smtpPort"
|
||||
type="number" placeholder="587" label="Port" />
|
||||
<x-forms.listbox id="smtpEncryption" label="Encryption" required
|
||||
<x-forms.listbox canGate="update" :canResource="$settings" id="smtpEncryption" label="Encryption" required
|
||||
:disabled="!auth()->user()->can('update', $settings)" :options="[
|
||||
['value' => 'starttls', 'label' => 'StartTLS'],
|
||||
['value' => 'tls', 'label' => 'TLS / SSL'],
|
||||
@@ -120,7 +120,7 @@
|
||||
<div class="application-settings-form">
|
||||
<x-application.settings-section title="Resend">
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<x-forms.listbox id="resendEnabled" label="Resend delivery"
|
||||
<x-forms.listbox canGate="update" :canResource="$settings" id="resendEnabled" label="Resend delivery"
|
||||
onChange="submitResend"
|
||||
:disabled="!auth()->user()->can('update', $settings)" :options="[
|
||||
['value' => true, 'label' => 'Enabled'],
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
|
||||
@if ($hasHttpsDomains && ! $labelsAreWritable)
|
||||
<div class="mt-4 max-w-md">
|
||||
<x-forms.listbox id="isForceHttpsEnabled" label="Redirect HTTP to HTTPS"
|
||||
<x-forms.listbox canGate="update" :canResource="$application" id="isForceHttpsEnabled" label="Redirect HTTP to HTTPS"
|
||||
onChange="updateForceHttps"
|
||||
helper="Disable only when Cloudflare Tunnel or another proxy connects to Coolify over HTTP. Keep enabled when Cloudflare uses Full or Full (Strict) SSL."
|
||||
:options="[
|
||||
@@ -119,7 +119,7 @@
|
||||
</x-slot:content>
|
||||
<form wire:submit="addDomain" class="application-settings-form flex flex-col gap-4">
|
||||
@if ($isCompose && count($composeServices) > 0)
|
||||
<x-forms.listbox label="Service" id="newDomainService" required
|
||||
<x-forms.listbox canGate="update" :canResource="$application" label="Service" id="newDomainService" required
|
||||
:options="collect($composeServices)->map(fn ($serviceName) => [
|
||||
'value' => $serviceName,
|
||||
'label' => $serviceName,
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
|
||||
<div class="w-full xl:hidden">
|
||||
@if (!($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw)))
|
||||
@can('deploy', $application)
|
||||
<div id="application-mobile-actions" class="relative mb-3"
|
||||
x-data="{ open: false }" @click.outside="open = false"
|
||||
@keydown.escape.window="open = false">
|
||||
@@ -149,6 +150,7 @@
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endcan
|
||||
@endif
|
||||
<div class="hidden" aria-hidden="true">
|
||||
<x-modal-confirmation title="Confirm Application Stopping?" buttonTitle="Stop"
|
||||
@@ -185,6 +187,7 @@
|
||||
<div class="resource-heading-menus shrink-0">
|
||||
<x-applications.links :application="$application" />
|
||||
</div>
|
||||
@can('deploy', $application)
|
||||
<div id="application-desktop-actions" class="relative" x-data="{ open: false }"
|
||||
x-effect="$dispatch('resource-actions-toggled', { open })"
|
||||
@click.outside="open = false" @keydown.escape.window="open = false">
|
||||
@@ -279,6 +282,7 @@
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endcan
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<div class="mt-4 grid gap-4 lg:grid-cols-2">
|
||||
<x-forms.input id="swarmReplicas" label="Replicas" required canGate="update"
|
||||
:canResource="$application" />
|
||||
<x-forms.listbox id="isSwarmOnlyWorkerNodes" label="Node placement" live onChange="instantSave"
|
||||
<x-forms.listbox canGate="update" :canResource="$application" id="isSwarmOnlyWorkerNodes" label="Node placement" live onChange="instantSave"
|
||||
:disabled="! auth()->user()->can('update', $application)" :options="[
|
||||
['value' => true, 'label' => 'Worker nodes only'],
|
||||
['value' => false, 'label' => 'Manager and worker nodes'],
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<div wire:key="public-access-{{ $publicPort ?: 'unset' }}">
|
||||
<x-forms.listbox id="isPublic" label="Access" live onChange="instantSave"
|
||||
:disabled="! auth()->user()->can('update', $database)" :options="[
|
||||
:disabled="! auth()->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 @@
|
||||
|
||||
<x-application.settings-section title="Log delivery"
|
||||
description="Forward container logs to the drain configured on the server.">
|
||||
<x-forms.listbox id="isLogDrainEnabled" label="Log drain" live onChange="instantSaveAdvanced"
|
||||
<x-forms.listbox canGate="update" :canResource="$database" id="isLogDrainEnabled" label="Log drain" live onChange="instantSaveAdvanced"
|
||||
:disabled="! auth()->user()->can('update', $database)" :options="[
|
||||
['value' => false, 'label' => 'Do not forward logs'],
|
||||
['value' => true, 'label' => 'Forward logs to the server drain'],
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<div wire:key="public-access-{{ $publicPort ?: 'unset' }}">
|
||||
<x-forms.listbox id="isPublic" label="Access" live onChange="instantSave"
|
||||
:disabled="! auth()->user()->can('update', $database)" :options="[
|
||||
:disabled="! auth()->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 @@
|
||||
|
||||
<x-application.settings-section title="Log delivery"
|
||||
description="Forward container logs to the drain configured on the server.">
|
||||
<x-forms.listbox id="isLogDrainEnabled" label="Log drain" live onChange="instantSaveAdvanced"
|
||||
<x-forms.listbox canGate="update" :canResource="$database" id="isLogDrainEnabled" label="Log drain" live onChange="instantSaveAdvanced"
|
||||
:disabled="! auth()->user()->can('update', $database)" :options="[
|
||||
['value' => false, 'label' => 'Do not forward logs'],
|
||||
['value' => true, 'label' => 'Forward logs to the server drain'],
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
|
||||
<div class="w-full xl:hidden">
|
||||
@if ($database->destination->server->isFunctional())
|
||||
@can('manage', $database)
|
||||
<div id="database-mobile-actions" class="relative mb-3"
|
||||
x-data="{ open: false }" @click.outside="open = false"
|
||||
@keydown.escape.window="open = false">
|
||||
@@ -127,6 +128,7 @@
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endcan
|
||||
@endif
|
||||
|
||||
</div>
|
||||
@@ -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">
|
||||
<div class="resource-heading-actions flex shrink-0 items-center gap-0.5">
|
||||
@if ($database->destination->server->isFunctional())
|
||||
@can('manage', $database)
|
||||
<div id="database-desktop-actions" class="flex items-center gap-0.5">
|
||||
@if (! $databaseStatus->startsWith('exited'))
|
||||
<button type="button" class="button button-highlighted"
|
||||
@@ -156,6 +159,7 @@
|
||||
</x-forms.button>
|
||||
@endif
|
||||
</div>
|
||||
@endcan
|
||||
@else
|
||||
<x-status-badge status="Server unavailable" type="error" />
|
||||
@endif
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<div wire:key="public-access-{{ $publicPort ?: 'unset' }}">
|
||||
<x-forms.listbox id="isPublic" label="Access" live onChange="instantSave"
|
||||
:disabled="! auth()->user()->can('update', $database)" :options="[
|
||||
:disabled="! auth()->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 @@
|
||||
|
||||
<x-application.settings-section title="Log delivery"
|
||||
description="Forward container logs to the drain configured on the server.">
|
||||
<x-forms.listbox id="isLogDrainEnabled" label="Log drain" live onChange="instantSaveAdvanced"
|
||||
<x-forms.listbox canGate="update" :canResource="$database" id="isLogDrainEnabled" label="Log drain" live onChange="instantSaveAdvanced"
|
||||
:disabled="! auth()->user()->can('update', $database)" :options="[
|
||||
['value' => false, 'label' => 'Do not forward logs'],
|
||||
['value' => true, 'label' => 'Forward logs to the server drain'],
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<div wire:key="public-access-{{ $publicPort ?: 'unset' }}">
|
||||
<x-forms.listbox id="isPublic" label="Access" live onChange="instantSave"
|
||||
:disabled="! auth()->user()->can('update', $database)" :options="[
|
||||
:disabled="! auth()->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 @@
|
||||
|
||||
<x-application.settings-section title="Log delivery"
|
||||
description="Forward container logs to the drain configured on the server.">
|
||||
<x-forms.listbox id="isLogDrainEnabled" label="Log drain" live onChange="instantSaveAdvanced"
|
||||
<x-forms.listbox canGate="update" :canResource="$database" id="isLogDrainEnabled" label="Log drain" live onChange="instantSaveAdvanced"
|
||||
:disabled="! auth()->user()->can('update', $database)" :options="[
|
||||
['value' => false, 'label' => 'Do not forward logs'],
|
||||
['value' => true, 'label' => 'Forward logs to the server drain'],
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<div wire:key="public-access-{{ $publicPort ?: 'unset' }}">
|
||||
<x-forms.listbox id="isPublic" label="Access" live onChange="instantSave"
|
||||
:disabled="! auth()->user()->can('update', $database)" :options="[
|
||||
:disabled="! auth()->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 @@
|
||||
|
||||
<x-application.settings-section title="Log delivery"
|
||||
description="Forward container logs to the drain configured on the server.">
|
||||
<x-forms.listbox id="isLogDrainEnabled" label="Log drain" live onChange="instantSaveAdvanced"
|
||||
<x-forms.listbox canGate="update" :canResource="$database" id="isLogDrainEnabled" label="Log drain" live onChange="instantSaveAdvanced"
|
||||
:disabled="! auth()->user()->can('update', $database)" :options="[
|
||||
['value' => false, 'label' => 'Do not forward logs'],
|
||||
['value' => true, 'label' => 'Forward logs to the server drain'],
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<div wire:key="public-access-{{ $publicPort ?: 'unset' }}">
|
||||
<x-forms.listbox id="isPublic" label="Access" live onChange="instantSave"
|
||||
:disabled="! auth()->user()->can('update', $database)" :options="[
|
||||
:disabled="! auth()->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 @@
|
||||
|
||||
<x-application.settings-section title="Log delivery"
|
||||
description="Forward container logs to the drain configured on the server.">
|
||||
<x-forms.listbox id="isLogDrainEnabled" label="Log drain" live onChange="instantSaveAdvanced"
|
||||
<x-forms.listbox canGate="update" :canResource="$database" id="isLogDrainEnabled" label="Log drain" live onChange="instantSaveAdvanced"
|
||||
:disabled="! auth()->user()->can('update', $database)" :options="[
|
||||
['value' => false, 'label' => 'Do not forward logs'],
|
||||
['value' => true, 'label' => 'Forward logs to the server drain'],
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<div wire:key="public-access-{{ $publicPort ?: 'unset' }}">
|
||||
<x-forms.listbox id="isPublic" label="Access" live onChange="instantSave" :onChangeArgs="[]"
|
||||
:disabled="! auth()->user()->can('update', $database)" :options="[
|
||||
:disabled="! auth()->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 @@
|
||||
|
||||
<x-application.settings-section id="log-delivery-section" title="Log delivery"
|
||||
description="Forward container logs to the drain configured on the server.">
|
||||
<x-forms.listbox id="isLogDrainEnabled" label="Log drain" live onChange="instantSaveAdvanced"
|
||||
<x-forms.listbox canGate="update" :canResource="$database" id="isLogDrainEnabled" label="Log drain" live onChange="instantSaveAdvanced"
|
||||
:disabled="! auth()->user()->can('update', $database)" :options="[
|
||||
['value' => false, 'label' => 'Do not forward logs'],
|
||||
['value' => true, 'label' => 'Forward logs to the server drain'],
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<div wire:key="public-access-{{ $publicPort ?: 'unset' }}">
|
||||
<x-forms.listbox id="isPublic" label="Access" live onChange="instantSave"
|
||||
:disabled="! auth()->user()->can('update', $database)" :options="[
|
||||
:disabled="! auth()->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 @@
|
||||
|
||||
<x-application.settings-section title="Log delivery"
|
||||
description="Forward container logs to the drain configured on the server.">
|
||||
<x-forms.listbox id="isLogDrainEnabled" label="Log drain" live onChange="instantSaveAdvanced"
|
||||
<x-forms.listbox canGate="update" :canResource="$database" id="isLogDrainEnabled" label="Log drain" live onChange="instantSaveAdvanced"
|
||||
:disabled="! auth()->user()->can('update', $database)" :options="[
|
||||
['value' => false, 'label' => 'Do not forward logs'],
|
||||
['value' => true, 'label' => 'Forward logs to the server drain'],
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
</x-slot:content>
|
||||
<form wire:submit="addDomain" class="application-settings-form flex flex-col gap-4">
|
||||
{{-- Always show which service receives the domain --}}
|
||||
<x-forms.listbox label="Service application" id="newServiceApplicationId" required
|
||||
<x-forms.listbox canGate="update" :canResource="$service" label="Service application" id="newServiceApplicationId" required
|
||||
helper="Domain will be assigned to this compose service application."
|
||||
:options="collect($serviceApps)->map(fn ($app) => [
|
||||
'value' => $app['id'],
|
||||
@@ -170,7 +170,7 @@
|
||||
<span class="min-w-0 flex-1 truncate text-sm font-medium text-black dark:text-white">{{ $heading }}</span>
|
||||
@if ($hasHttpsDomains)
|
||||
<div class="w-full sm:w-72">
|
||||
<x-forms.listbox id="forceHttpsRedirects.{{ $appId }}"
|
||||
<x-forms.listbox canGate="update" :canResource="$service" id="forceHttpsRedirects.{{ $appId }}"
|
||||
htmlId="service-force-https-{{ $appId }}"
|
||||
label="Redirect HTTP to HTTPS" onChange="updateForceHttps"
|
||||
:onChangeArgs="[(int) $appId]"
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
|
||||
<div class="w-full xl:hidden">
|
||||
@if ($service->isDeployable)
|
||||
@can('deploy', $service)
|
||||
<div id="service-mobile-actions" class="relative mb-3"
|
||||
x-data="{ open: false }" @click.outside="open = false"
|
||||
@keydown.escape.window="open = false">
|
||||
@@ -165,6 +166,7 @@
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endcan
|
||||
@else
|
||||
<a href="{{ $environmentVariablesUrl }}" {{ wireNavigate() }}
|
||||
class="mb-3 inline-flex" aria-label="Open required environment variables">
|
||||
@@ -183,6 +185,7 @@
|
||||
<div class="resource-heading-menus shrink-0">
|
||||
<x-services.links :service="$service" />
|
||||
</div>
|
||||
@can('deploy', $service)
|
||||
<div id="service-desktop-actions" class="relative" x-data="{ open: false }"
|
||||
x-effect="$dispatch('resource-actions-toggled', { open })"
|
||||
@click.outside="open = false" @keydown.escape.window="open = false">
|
||||
@@ -247,6 +250,7 @@
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endcan
|
||||
@else
|
||||
<a href="{{ $environmentVariablesUrl }}" {{ wireNavigate() }}
|
||||
aria-label="Open required environment variables">
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
<x-application.settings-section title="Network"
|
||||
description="Control whether this Compose stack joins Coolify's predefined network.">
|
||||
<x-forms.listbox id="connectToDockerNetwork" label="Network attachment" live onChange="instantSave"
|
||||
<x-forms.listbox canGate="update" :canResource="$service" id="connectToDockerNetwork" label="Network attachment" live onChange="instantSave"
|
||||
:disabled="! auth()->user()->can('update', $service)" :options="[
|
||||
['value' => false, 'label' => 'Use the stack network only'],
|
||||
['value' => true, 'label' => 'Connect to the predefined Coolify network'],
|
||||
|
||||
@@ -27,21 +27,21 @@
|
||||
</x-slot:actions>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<x-forms.input :disabled="!auth()->user()->can('update', $resource)" placeholder="Name" id="name"
|
||||
<x-forms.input canGate="update" :canResource="$resource" :disabled="!auth()->user()->can('update', $resource)" placeholder="Name" id="name"
|
||||
label="Name" required />
|
||||
<x-forms.input :disabled="!auth()->user()->can('update', $resource)"
|
||||
<x-forms.input canGate="update" :canResource="$resource" :disabled="!auth()->user()->can('update', $resource)"
|
||||
placeholder="0 0 * * * or daily" id="frequency" label="Schedule"
|
||||
helper="Use every_minute, hourly, daily, weekly, monthly, yearly, or a cron expression."
|
||||
required />
|
||||
<x-forms.input :disabled="!auth()->user()->can('update', $resource)" type="number"
|
||||
<x-forms.input canGate="update" :canResource="$resource" :disabled="!auth()->user()->can('update', $resource)" type="number"
|
||||
placeholder="300" id="timeout" helper="Maximum execution time from 60 to 36,000 seconds."
|
||||
label="Timeout (seconds)" required />
|
||||
<x-forms.input :disabled="!auth()->user()->can('update', $resource)" placeholder="php"
|
||||
<x-forms.input canGate="update" :canResource="$resource" :disabled="!auth()->user()->can('update', $resource)" placeholder="php"
|
||||
helper="Leave empty when the resource only has one container." id="container"
|
||||
label="{{ $type === 'service' ? 'Service' : 'Container' }}" />
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<x-forms.input :disabled="!auth()->user()->can('update', $resource)"
|
||||
<x-forms.input canGate="update" :canResource="$resource" :disabled="!auth()->user()->can('update', $resource)"
|
||||
placeholder="php artisan schedule:run" id="command" label="Command" required />
|
||||
</div>
|
||||
</x-application.settings-section>
|
||||
|
||||
@@ -39,13 +39,13 @@
|
||||
@endif
|
||||
</div>
|
||||
<div class="application-settings-section-body grid gap-4 sm:grid-cols-2">
|
||||
<x-forms.listbox id="s3StorageId" label="S3 storage" :required="$saveToS3"
|
||||
<x-forms.listbox canGate="update" :canResource="$resource" id="s3StorageId" label="S3 storage" :required="$saveToS3"
|
||||
:disabled="! auth()->user()?->can('update', $resource)"
|
||||
:options="$availableS3Storages->map(fn ($s3Storage) => [
|
||||
'value' => $s3Storage->id,
|
||||
'label' => $s3Storage->name,
|
||||
])->values()->all()" />
|
||||
<x-forms.listbox id="disableLocalBackup" label="Local copy"
|
||||
<x-forms.listbox canGate="update" :canResource="$resource" id="disableLocalBackup" label="Local copy"
|
||||
:disabled="! $saveToS3 || ! auth()->user()?->can('update', $resource)" :options="[
|
||||
['value' => false, 'label' => 'Keep local backup'],
|
||||
['value' => true, 'label' => 'Delete after S3 upload'],
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<x-application.settings-section id="server-new-relic-drain-section" title="New Relic"
|
||||
helper="Send logs through the New Relic Log API.">
|
||||
<div class="grid gap-4 lg:grid-cols-3">
|
||||
<x-forms.listbox id="isLogDrainNewRelicEnabled" label="Status"
|
||||
<x-forms.listbox canGate="update" :canResource="$server" id="isLogDrainNewRelicEnabled" label="Status"
|
||||
onChange="instantSave" :options="[
|
||||
['value' => false, 'label' => 'Disabled'],
|
||||
['value' => true, 'label' => 'Enabled'],
|
||||
@@ -48,7 +48,7 @@
|
||||
<x-application.settings-section id="server-axiom-drain-section" title="Axiom"
|
||||
helper="Send logs to an Axiom dataset using its ingest API.">
|
||||
<div class="grid gap-4 lg:grid-cols-3">
|
||||
<x-forms.listbox id="isLogDrainAxiomEnabled" label="Status"
|
||||
<x-forms.listbox canGate="update" :canResource="$server" id="isLogDrainAxiomEnabled" label="Status"
|
||||
onChange="instantSave" :options="[
|
||||
['value' => false, 'label' => 'Disabled'],
|
||||
['value' => true, 'label' => 'Enabled'],
|
||||
@@ -65,7 +65,7 @@
|
||||
<x-application.settings-section id="server-custom-drain-section" title="Custom Fluent Bit"
|
||||
helper="Provide a custom Fluent Bit output and optional parser configuration.">
|
||||
<div class="mb-4 max-w-sm">
|
||||
<x-forms.listbox id="isLogDrainCustomEnabled" label="Status"
|
||||
<x-forms.listbox canGate="update" :canResource="$server" id="isLogDrainCustomEnabled" label="Status"
|
||||
onChange="instantSave" :options="[
|
||||
['value' => false, 'label' => 'Disabled'],
|
||||
['value' => true, 'label' => 'Enabled'],
|
||||
|
||||
@@ -23,14 +23,14 @@
|
||||
</x-callout>
|
||||
|
||||
<div class="mt-4 grid gap-4 lg:grid-cols-2">
|
||||
<x-forms.listbox id="isSwarmManager" label="Manager role"
|
||||
<x-forms.listbox canGate="update" :canResource="$server" id="isSwarmManager" label="Manager role"
|
||||
helper="Managers control scheduling and cluster state." onChange="instantSave"
|
||||
:options="[
|
||||
['value' => false, 'label' => 'Not a Swarm manager'],
|
||||
['value' => true, 'label' => 'Swarm manager'],
|
||||
]"
|
||||
:disabled="$server->settings->is_swarm_worker || !auth()->user()->can('update', $server)" />
|
||||
<x-forms.listbox id="isSwarmWorker" label="Worker role"
|
||||
<x-forms.listbox canGate="update" :canResource="$server" id="isSwarmWorker" label="Worker role"
|
||||
helper="Workers run tasks assigned by a Swarm manager." onChange="instantSave"
|
||||
:options="[
|
||||
['value' => false, 'label' => 'Not a Swarm worker'],
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
|
||||
@if (!isCloud())
|
||||
<div class="lg:col-span-2">
|
||||
<x-forms.listbox id="isSystemWide" label="Availability" :options="[
|
||||
<x-forms.listbox canGate="update" :canResource="$github_app" id="isSystemWide" label="Availability" :options="[
|
||||
['value' => false, 'label' => 'Only this team'],
|
||||
['value' => true, 'label' => 'Every team on this instance'],
|
||||
]"
|
||||
@@ -161,7 +161,7 @@
|
||||
label="Client secret" type="password" required />
|
||||
<x-forms.input canGate="update" :canResource="$github_app" id="webhookSecret"
|
||||
label="Webhook secret" type="password" required />
|
||||
<x-forms.listbox id="privateKeyId" label="Private key" required
|
||||
<x-forms.listbox canGate="update" :canResource="$github_app" id="privateKeyId" label="Private key" required
|
||||
:options="$privateKeyOptions" :disabled="!auth()->user()->can('update', $github_app)" />
|
||||
</div>
|
||||
</x-application.settings-section>
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
<x-forms.input canGate="update" :canResource="$gitlab_app" type="number" id="customPort"
|
||||
label="SSH port" />
|
||||
<div class="lg:col-span-2">
|
||||
<x-forms.listbox id="privateKeyId" label="SSH private key (optional)"
|
||||
<x-forms.listbox canGate="update" :canResource="$gitlab_app" id="privateKeyId" label="SSH private key (optional)"
|
||||
:options="collect($privateKeys)->map(fn ($key) => [
|
||||
'value' => $key->id,
|
||||
'label' => $key->name,
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<x-forms.input id="name" label="Name" required canGate="update" :canResource="$team" />
|
||||
<x-forms.input id="description" label="Description" canGate="update" :canResource="$team" />
|
||||
<div class="lg:col-span-2">
|
||||
<x-forms.listbox id="is_mcp_server_enabled" label="MCP server"
|
||||
<x-forms.listbox canGate="update" :canResource="$team" id="is_mcp_server_enabled" label="MCP server"
|
||||
helper="Controls whether this team's API tokens can use the instance MCP endpoint."
|
||||
:disabled="! auth()->user()->can('update', $team)" :options="[
|
||||
['value' => false, 'label' => 'Disabled for this team'],
|
||||
|
||||
@@ -22,6 +22,15 @@ test('listbox trigger height matches shared inputs', function () {
|
||||
->toMatch('/\.application-settings-workspace \.listbox-trigger[^}]*height: 2rem;/s');
|
||||
});
|
||||
|
||||
test('disabled listboxes use the same colors as disabled inputs', function () {
|
||||
$css = file_get_contents(resource_path('css/app.css'));
|
||||
|
||||
expect($css)
|
||||
->toMatch('/\.listbox-trigger:disabled \{[^}]*background-color: var\(--color-neutral-100\);[^}]*color: var\(--color-neutral-400\);/s')
|
||||
->toMatch('/\.dark \.listbox-trigger:disabled \{[^}]*background-color: color-mix\(in oklab, var\(--color-white\) 3%, transparent\);[^}]*color: var\(--color-fg-faint\);/s')
|
||||
->not->toMatch('/\.listbox-trigger:disabled \{[^}]*opacity:/s');
|
||||
});
|
||||
|
||||
test('listbox component uses shared trigger label truncation', function () {
|
||||
$html = Blade::render(<<<'BLADE'
|
||||
<x-forms.listbox id="longOption" label="Example"
|
||||
|
||||
@@ -1,9 +1,58 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('auto-disables listboxes when the gate denies access', function () {
|
||||
Gate::define('update-listbox-test', fn (): bool => false);
|
||||
|
||||
$html = Blade::render(<<<'BLADE'
|
||||
<x-forms.listbox id="status" :wire="false" value="enabled"
|
||||
canGate="update-listbox-test" :canResource="new stdClass"
|
||||
:options="[['value' => 'enabled', 'label' => 'Enabled']]" />
|
||||
BLADE);
|
||||
|
||||
expect($html)->toMatch('/<button[^>]*id="status-trigger"[^>]*\sdisabled(?:[=\s>])/');
|
||||
});
|
||||
|
||||
it('declares gate attributes on form controls with update permission checks', function () {
|
||||
$controlPattern = '/<x-forms\.(?:listbox|input|select|checkbox|textarea|button|toggle)\b.*?(?:\/>|<\/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]*?<div id=\"{$prefix}-{$viewport}-actions\"/"
|
||||
);
|
||||
}
|
||||
})->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',
|
||||
[
|
||||
'/<x-forms\.listbox id="isPublic"[\s\S]*?:disabled="! auth\(\)->user\(\)->can\(\'update\', \$database\)" canGate="update" :canResource="\$database" :options=/',
|
||||
],
|
||||
],
|
||||
'redis public access control' => [
|
||||
'resources/views/livewire/project/database/redis/general.blade.php',
|
||||
['/<x-forms\.listbox id="isPublic"[\s\S]*?canGate="update" :canResource="\$database" :options=/'],
|
||||
],
|
||||
'mongodb public access control' => [
|
||||
'resources/views/livewire/project/database/mongodb/general.blade.php',
|
||||
['/<x-forms\.listbox id="isPublic"[\s\S]*?canGate="update" :canResource="\$database" :options=/'],
|
||||
],
|
||||
'clickhouse public access control' => [
|
||||
'resources/views/livewire/project/database/clickhouse/general.blade.php',
|
||||
['/<x-forms\.listbox id="isPublic"[\s\S]*?canGate="update" :canResource="\$database" :options=/'],
|
||||
],
|
||||
'mariadb public access control' => [
|
||||
'resources/views/livewire/project/database/mariadb/general.blade.php',
|
||||
['/<x-forms\.listbox id="isPublic"[\s\S]*?canGate="update" :canResource="\$database" :options=/'],
|
||||
],
|
||||
'dragonfly public access control' => [
|
||||
'resources/views/livewire/project/database/dragonfly/general.blade.php',
|
||||
['/<x-forms\.listbox id="isPublic"[\s\S]*?canGate="update" :canResource="\$database" :options=/'],
|
||||
],
|
||||
'mysql public access control' => [
|
||||
'resources/views/livewire/project/database/mysql/general.blade.php',
|
||||
['/<x-forms\.listbox id="isPublic"[\s\S]*?canGate="update" :canResource="\$database" :options=/'],
|
||||
],
|
||||
'keydb public access control' => [
|
||||
'resources/views/livewire/project/database/keydb/general.blade.php',
|
||||
['/<x-forms\.listbox id="isPublic"[\s\S]*?canGate="update" :canResource="\$database" :options=/'],
|
||||
],
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user