fix(database): gate import form controls by update access

Require database import form controls to declare update authorization against the resource and add coverage to prevent unguarded controls.
This commit is contained in:
Andras Bacsai 2026-05-28 20:48:18 +02:00
parent 902a60239d
commit eb7da5c082
2 changed files with 34 additions and 14 deletions

View file

@ -58,9 +58,9 @@
@if ($resourceDbType === 'standalone-postgresql') @if ($resourceDbType === 'standalone-postgresql')
@if ($dumpAll) @if ($dumpAll)
<x-forms.textarea rows="6" readonly label="Custom Import Command" <x-forms.textarea rows="6" readonly label="Custom Import Command"
wire:model='restoreCommandText'></x-forms.textarea> wire:model='restoreCommandText' canGate="update" :canResource="$this->resource"></x-forms.textarea>
@else @else
<x-forms.input label="Custom Import Command" wire:model='postgresqlRestoreCommand'></x-forms.input> <x-forms.input label="Custom Import Command" wire:model='postgresqlRestoreCommand' canGate="update" :canResource="$this->resource"></x-forms.input>
<div class="flex flex-col gap-1 pt-1"> <div class="flex flex-col gap-1 pt-1">
<span class="text-xs">You can add "--clean" to drop objects before creating them, avoiding <span class="text-xs">You can add "--clean" to drop objects before creating them, avoiding
conflicts.</span> conflicts.</span>
@ -68,27 +68,27 @@
</div> </div>
@endif @endif
<div class="w-64 pt-2"> <div class="w-64 pt-2">
<x-forms.checkbox label="Backup includes all databases" wire:model.live='dumpAll'></x-forms.checkbox> <x-forms.checkbox label="Backup includes all databases" wire:model.live='dumpAll' canGate="update" :canResource="$this->resource"></x-forms.checkbox>
</div> </div>
@elseif ($resourceDbType === 'standalone-mysql') @elseif ($resourceDbType === 'standalone-mysql')
@if ($dumpAll) @if ($dumpAll)
<x-forms.textarea rows="14" readonly label="Custom Import Command" <x-forms.textarea rows="14" readonly label="Custom Import Command"
wire:model='restoreCommandText'></x-forms.textarea> wire:model='restoreCommandText' canGate="update" :canResource="$this->resource"></x-forms.textarea>
@else @else
<x-forms.input label="Custom Import Command" wire:model='mysqlRestoreCommand'></x-forms.input> <x-forms.input label="Custom Import Command" wire:model='mysqlRestoreCommand' canGate="update" :canResource="$this->resource"></x-forms.input>
@endif @endif
<div class="w-64 pt-2"> <div class="w-64 pt-2">
<x-forms.checkbox label="Backup includes all databases" wire:model.live='dumpAll'></x-forms.checkbox> <x-forms.checkbox label="Backup includes all databases" wire:model.live='dumpAll' canGate="update" :canResource="$this->resource"></x-forms.checkbox>
</div> </div>
@elseif ($resourceDbType === 'standalone-mariadb') @elseif ($resourceDbType === 'standalone-mariadb')
@if ($dumpAll) @if ($dumpAll)
<x-forms.textarea rows="14" readonly label="Custom Import Command" <x-forms.textarea rows="14" readonly label="Custom Import Command"
wire:model='restoreCommandText'></x-forms.textarea> wire:model='restoreCommandText' canGate="update" :canResource="$this->resource"></x-forms.textarea>
@else @else
<x-forms.input label="Custom Import Command" wire:model='mariadbRestoreCommand'></x-forms.input> <x-forms.input label="Custom Import Command" wire:model='mariadbRestoreCommand' canGate="update" :canResource="$this->resource"></x-forms.input>
@endif @endif
<div class="w-64 pt-2"> <div class="w-64 pt-2">
<x-forms.checkbox label="Backup includes all databases" wire:model.live='dumpAll'></x-forms.checkbox> <x-forms.checkbox label="Backup includes all databases" wire:model.live='dumpAll' canGate="update" :canResource="$this->resource"></x-forms.checkbox>
</div> </div>
@endif @endif
@ -128,8 +128,8 @@ class="flex-1 p-6 border-2 rounded-sm cursor-pointer transition-all"
<h3>Backup File</h3> <h3>Backup File</h3>
<form class="flex gap-2 items-end pt-2"> <form class="flex gap-2 items-end pt-2">
<x-forms.input label="Location of the backup file on the server" placeholder="e.g. /home/user/backup.sql.gz" <x-forms.input label="Location of the backup file on the server" placeholder="e.g. /home/user/backup.sql.gz"
wire:model='customLocation' x-model="$wire.customLocation"></x-forms.input> wire:model='customLocation' x-model="$wire.customLocation" canGate="update" :canResource="$this->resource"></x-forms.input>
<x-forms.button class="w-full" wire:click='checkFile' x-bind:disabled="!$wire.customLocation">Check File</x-forms.button> <x-forms.button class="w-full" wire:click='checkFile' x-bind:disabled="!$wire.customLocation" canGate="update" :canResource="$this->resource">Check File</x-forms.button>
</form> </form>
<div class="pt-2 text-center text-xl font-bold"> <div class="pt-2 text-center text-xl font-bold">
Or Or
@ -168,7 +168,7 @@ class="flex-1 p-6 border-2 rounded-sm cursor-pointer transition-all"
<div x-show="restoreType === 's3'" class="pt-6"> <div x-show="restoreType === 's3'" class="pt-6">
<h3>Restore from S3</h3> <h3>Restore from S3</h3>
<div class="flex flex-col gap-2 pt-2"> <div class="flex flex-col gap-2 pt-2">
<x-forms.select label="S3 Storage" wire:model.live="s3StorageId"> <x-forms.select label="S3 Storage" wire:model.live="s3StorageId" canGate="update" :canResource="$this->resource">
<option value="">Select S3 Storage</option> <option value="">Select S3 Storage</option>
@foreach ($availableS3Storages as $storage) @foreach ($availableS3Storages as $storage)
<option value="{{ $storage['id'] }}">{{ $storage['name'] }} <option value="{{ $storage['id'] }}">{{ $storage['name'] }}
@ -182,10 +182,10 @@ class="flex-1 p-6 border-2 rounded-sm cursor-pointer transition-all"
<x-forms.input label="S3 File Path (within bucket)" <x-forms.input label="S3 File Path (within bucket)"
helper="Path to the backup file in your S3 bucket, e.g., /backups/database-2025-01-15.gz" helper="Path to the backup file in your S3 bucket, e.g., /backups/database-2025-01-15.gz"
placeholder="/backups/database-backup.gz" wire:model.blur='s3Path' placeholder="/backups/database-backup.gz" wire:model.blur='s3Path'
wire:keydown.enter='checkS3File'></x-forms.input> wire:keydown.enter='checkS3File' canGate="update" :canResource="$this->resource"></x-forms.input>
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.button class="w-full" wire:click='checkS3File' x-bind:disabled="!s3StorageId || !s3Path"> <x-forms.button class="w-full" wire:click='checkS3File' x-bind:disabled="!s3StorageId || !s3Path" canGate="update" :canResource="$this->resource">
Check File Check File
</x-forms.button> </x-forms.button>
</div> </div>

View file

@ -0,0 +1,20 @@
<?php
it('declares explicit authorization on database import form controls', function () {
$view = file_get_contents(resource_path('views/livewire/project/database/import-form.blade.php'));
preg_match_all(
'/<x-forms\.(button|input|select|checkbox|textarea)\b[^>]*>/s',
$view,
$matches,
PREG_OFFSET_CAPTURE
);
$missingAuthorization = collect($matches[0])
->filter(fn (array $match): bool => ! str_contains($match[0], 'canGate=') || ! str_contains($match[0], 'canResource='))
->map(fn (array $match): string => 'Line '.(substr_count(substr($view, 0, $match[1]), PHP_EOL) + 1).': '.trim(preg_replace('/\s+/', ' ', $match[0])))
->values()
->all();
expect($missingAuthorization)->toBeEmpty();
});