fix(livewire): authorize exposed resource mutations (#11596)

This commit is contained in:
Andras Bacsai 2026-09-02 19:58:28 +02:00 committed by GitHub
parent e2e91fbb85
commit b8866b87e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 179 additions and 1 deletions

View file

@ -651,6 +651,8 @@ public function generateNginxConfiguration($type = 'static')
public function resetDefaultLabels($manualReset = false) public function resetDefaultLabels($manualReset = false)
{ {
$this->authorize('update', $this->application);
try { try {
if (! $this->isContainerLabelReadonlyEnabled && ! $manualReset) { if (! $this->isContainerLabelReadonlyEnabled && ! $manualReset) {
return; return;

View file

@ -35,6 +35,12 @@ public function getListeners()
public function activityFinished() public function activityFinished()
{ {
if (auth()->user()->cannot('update', $this->database)) {
$this->dispatch('refresh');
return;
}
try { try {
// Only set started_at if database is actually running // Only set started_at if database is actually running
if ($this->database->isRunning()) { if ($this->database->isRunning()) {

View file

@ -145,6 +145,8 @@ public function refresh()
*/ */
public function loadValues(): void public function loadValues(): void
{ {
$this->authorize('update', $this->env);
if ($this->valuesLoaded) { if ($this->valuesLoaded) {
return; return;
} }

View file

@ -17,12 +17,15 @@
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis; use App\Models\StandaloneRedis;
use App\Support\ValidationPatterns; use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Process; use Illuminate\Support\Facades\Process;
use Livewire\Attributes\Locked; use Livewire\Attributes\Locked;
use Livewire\Component; use Livewire\Component;
class GetLogs extends Component class GetLogs extends Component
{ {
use AuthorizesRequests;
public const MAX_LOG_LINES = 50000; public const MAX_LOG_LINES = 50000;
public const MAX_DISPLAY_SIZE_BYTES = 5 * 1024 * 1024; public const MAX_DISPLAY_SIZE_BYTES = 5 * 1024 * 1024;
@ -82,6 +85,10 @@ public function mount()
public function instantSave() public function instantSave()
{ {
if (! is_null($this->resource)) { if (! is_null($this->resource)) {
if (auth()->user()->cannot('update', $this->resource)) {
return;
}
if ($this->resource->getMorphClass() === Application::class) { if ($this->resource->getMorphClass() === Application::class) {
$this->resource->settings->is_include_timestamps = $this->showTimeStamps; $this->resource->settings->is_include_timestamps = $this->showTimeStamps;
$this->resource->settings->save(); $this->resource->settings->save();

View file

@ -28,6 +28,8 @@ public function getListeners()
public function loadScripts() public function loadScripts()
{ {
$this->authorize('viewAny', CloudInitScript::class);
CloudInitScript::ownedByCurrentTeam() CloudInitScript::ownedByCurrentTeam()
->whereNull('uuid') ->whereNull('uuid')
->get() ->get()

View file

@ -94,6 +94,7 @@ private function validateToken(string $provider, string $token): bool
public function addToken() public function addToken()
{ {
$this->authorize('create', CloudProviderToken::class);
$this->validate(); $this->validate();
try { try {

View file

@ -53,6 +53,8 @@ class ValidateAndInstall extends Component
public function init(int $data = 0) public function init(int $data = 0)
{ {
$this->authorize('update', $this->server);
if (! $this->server->canBeValidated()) { if (! $this->server->canBeValidated()) {
$this->error = 'This server was transferred to another Coolify instance and cannot be revalidated here.'; $this->error = 'This server was transferred to another Coolify instance and cannot be revalidated here.';
$this->server->update([ $this->server->update([
@ -160,6 +162,8 @@ public function validateConnection()
public function validateOS() public function validateOS()
{ {
$this->authorize('update', $this->server);
$this->supported_os_type = $this->server->validateOS(); $this->supported_os_type = $this->server->validateOS();
if (! $this->supported_os_type) { if (! $this->supported_os_type) {
$this->error = 'Server OS type is not supported. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.'; $this->error = 'Server OS type is not supported. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
@ -174,6 +178,8 @@ public function validateOS()
public function validatePrerequisites() public function validatePrerequisites()
{ {
$this->authorize('update', $this->server);
$validationResult = $this->server->validatePrerequisites(); $validationResult = $this->server->validatePrerequisites();
$this->prerequisites_installed = $validationResult['success']; $this->prerequisites_installed = $validationResult['success'];
if (! $validationResult['success']) { if (! $validationResult['success']) {
@ -212,6 +218,8 @@ public function validatePrerequisites()
public function validateDockerEngine() public function validateDockerEngine()
{ {
$this->authorize('update', $this->server);
$this->docker_installed = $this->server->validateDockerEngine(); $this->docker_installed = $this->server->validateDockerEngine();
$this->docker_compose_installed = $this->server->validateDockerCompose(); $this->docker_compose_installed = $this->server->validateDockerCompose();
if (! $this->docker_installed || ! $this->docker_compose_installed) { if (! $this->docker_installed || ! $this->docker_compose_installed) {
@ -248,6 +256,8 @@ public function validateDockerEngine()
public function validateDockerVersion() public function validateDockerVersion()
{ {
$this->authorize('update', $this->server);
if ($this->server->isSwarm()) { if ($this->server->isSwarm()) {
$swarmInstalled = $this->server->validateDockerSwarm(); $swarmInstalled = $this->server->validateDockerSwarm();
if ($swarmInstalled) { if ($swarmInstalled) {

View file

@ -1,8 +1,11 @@
<?php <?php
use App\Livewire\Project\Application\Advanced as ApplicationAdvanced; use App\Livewire\Project\Application\Advanced as ApplicationAdvanced;
use App\Livewire\Project\Application\General as ApplicationGeneral;
use App\Livewire\Project\Application\Heading as ApplicationHeading; use App\Livewire\Project\Application\Heading as ApplicationHeading;
use App\Livewire\Project\Application\Rollback as ApplicationRollback; use App\Livewire\Project\Application\Rollback as ApplicationRollback;
use App\Livewire\Project\Shared\EnvironmentVariable\Show as EnvironmentVariableShow;
use App\Livewire\Project\Shared\GetLogs;
use App\Models\Application; use App\Models\Application;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\Project; use App\Models\Project;
@ -10,6 +13,7 @@
use App\Models\StandaloneDocker; use App\Models\StandaloneDocker;
use App\Models\Team; use App\Models\Team;
use App\Models\User; use App\Models\User;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@ -169,6 +173,60 @@
expect($this->member->can('update', $this->application))->toBeFalse(); expect($this->member->can('update', $this->application))->toBeFalse();
}); });
test('member cannot reset application labels', function () {
$this->actingAs($this->member);
session(['currentTeam' => $this->team]);
$originalLabels = $this->application->custom_labels;
$component = app(ApplicationGeneral::class);
$component->application = $this->application;
$component->isContainerLabelReadonlyEnabled = true;
expect(fn () => $component->resetDefaultLabels(true))
->toThrow(AuthorizationException::class);
expect($this->application->fresh()->custom_labels)->toBe($originalLabels);
});
test('member cannot load application environment variable values', function () {
$this->actingAs($this->member);
session(['currentTeam' => $this->team]);
$environmentVariable = $this->application->environment_variables()->create([
'key' => 'SECRET_KEY',
'value' => 'super-secret-value',
'is_preview' => false,
]);
Livewire::test(EnvironmentVariableShow::class, [
'env' => $environmentVariable,
'type' => 'application',
])
->call('loadValues')
->assertForbidden()
->assertSet('valuesLoaded', false)
->assertSet('value', null);
});
test('member cannot save application log timestamp settings', function () {
$this->actingAs($this->member);
session(['currentTeam' => $this->team]);
$originalValue = $this->application->settings->is_include_timestamps;
Livewire::test(GetLogs::class, [
'resource' => $this->application,
'server' => $this->server,
'container' => $this->application->uuid,
])
->set('showTimeStamps', ! $originalValue)
->call('instantSave')
->assertSuccessful();
expect($this->application->settings->fresh()->is_include_timestamps)->toBe($originalValue);
});
// --- Application Advanced Livewire actions --- // --- Application Advanced Livewire actions ---
test('member cannot save application advanced settings', function () { test('member cannot save application advanced settings', function () {

View file

@ -1,5 +1,6 @@
<?php <?php
use App\Livewire\Project\Database\Heading;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\Project; use App\Models\Project;
use App\Models\Server; use App\Models\Server;
@ -10,11 +11,12 @@
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Livewire\Livewire;
uses(RefreshDatabase::class); uses(RefreshDatabase::class);
beforeEach(function () { beforeEach(function () {
InstanceSettings::updateOrCreate(['id' => 0]); InstanceSettings::unguarded(fn () => InstanceSettings::updateOrCreate(['id' => 0], ['id' => 0]));
$this->team = Team::factory()->create(); $this->team = Team::factory()->create();
@ -89,6 +91,18 @@
expect($this->member->can('update', $this->database))->toBeFalse(); expect($this->member->can('update', $this->database))->toBeFalse();
}); });
test('member cannot persist database activity status', function () {
$this->actingAs($this->member);
session(['currentTeam' => $this->team]);
Livewire::test(Heading::class, ['database' => $this->database])
->call('activityFinished')
->assertSuccessful()
->assertDispatched('refresh');
expect($this->database->fresh()->started_at)->toBeNull();
});
// --- Database Policy: delete --- // --- Database Policy: delete ---
test('admin can delete database', function () { test('admin can delete database', function () {

View file

@ -0,0 +1,57 @@
<?php
use App\Livewire\Security\CloudInitScripts;
use App\Livewire\Security\CloudProviderTokenForm;
use App\Models\CloudInitScript;
use App\Models\CloudProviderToken;
use App\Models\InstanceSettings;
use App\Models\Team;
use App\Models\User;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Livewire\Livewire;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->withoutVite();
InstanceSettings::unguarded(fn () => InstanceSettings::updateOrCreate(['id' => 0], ['id' => 0]));
$this->team = Team::factory()->create();
$this->member = User::factory()->create();
$this->member->teams()->attach($this->team, ['role' => 'member']);
$this->actingAs($this->member);
session(['currentTeam' => $this->team]);
});
test('member cannot create a cloud provider token through the public action', function () {
Http::fake(['*' => Http::response([], 200)]);
Livewire::test(CloudProviderTokenForm::class)
->set('provider', 'hetzner')
->set('token', 'secret-token')
->set('name', 'Unauthorized token')
->call('addToken')
->assertForbidden();
expect(CloudProviderToken::query()->count())->toBe(0);
});
test('member cannot load cloud-init scripts through the public action', function () {
$script = CloudInitScript::query()->create([
'team_id' => $this->team->id,
'name' => 'Protected script',
'script' => '#cloud-config',
]);
CloudInitScript::query()->whereKey($script)->update(['uuid' => null]);
$component = app(CloudInitScripts::class);
expect(fn () => $component->loadScripts())
->toThrow(AuthorizationException::class);
expect($script->refresh()->uuid)->toBeNull();
});

View file

@ -5,6 +5,7 @@
use App\Livewire\Server\Index as ServerIndex; use App\Livewire\Server\Index as ServerIndex;
use App\Livewire\Server\LogDrains; use App\Livewire\Server\LogDrains;
use App\Livewire\Server\Navbar as ServerNavbar; use App\Livewire\Server\Navbar as ServerNavbar;
use App\Livewire\Server\ValidateAndInstall;
use App\Models\CloudProviderToken; use App\Models\CloudProviderToken;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\Server; use App\Models\Server;
@ -61,6 +62,24 @@
expect($this->member->can('update', $this->server))->toBeFalse(); expect($this->member->can('update', $this->server))->toBeFalse();
}); });
test('member cannot invoke server validation write steps', function (string $method) {
$this->actingAs($this->member);
session(['currentTeam' => $this->team]);
$before = $this->server->fresh()->getAttributes();
Livewire::test(ValidateAndInstall::class, ['server' => $this->server])
->call($method)
->assertForbidden();
expect($this->server->fresh()->getAttributes())->toBe($before);
})->with([
'init',
'validateOS',
'validatePrerequisites',
'validateDockerEngine',
'validateDockerVersion',
]);
// --- Server Policy: delete --- // --- Server Policy: delete ---
test('admin can delete server', function () { test('admin can delete server', function () {