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)
{
$this->authorize('update', $this->application);
try {
if (! $this->isContainerLabelReadonlyEnabled && ! $manualReset) {
return;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -53,6 +53,8 @@ class ValidateAndInstall extends Component
public function init(int $data = 0)
{
$this->authorize('update', $this->server);
if (! $this->server->canBeValidated()) {
$this->error = 'This server was transferred to another Coolify instance and cannot be revalidated here.';
$this->server->update([
@ -160,6 +162,8 @@ public function validateConnection()
public function validateOS()
{
$this->authorize('update', $this->server);
$this->supported_os_type = $this->server->validateOS();
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>.';
@ -174,6 +178,8 @@ public function validateOS()
public function validatePrerequisites()
{
$this->authorize('update', $this->server);
$validationResult = $this->server->validatePrerequisites();
$this->prerequisites_installed = $validationResult['success'];
if (! $validationResult['success']) {
@ -212,6 +218,8 @@ public function validatePrerequisites()
public function validateDockerEngine()
{
$this->authorize('update', $this->server);
$this->docker_installed = $this->server->validateDockerEngine();
$this->docker_compose_installed = $this->server->validateDockerCompose();
if (! $this->docker_installed || ! $this->docker_compose_installed) {
@ -248,6 +256,8 @@ public function validateDockerEngine()
public function validateDockerVersion()
{
$this->authorize('update', $this->server);
if ($this->server->isSwarm()) {
$swarmInstalled = $this->server->validateDockerSwarm();
if ($swarmInstalled) {

View file

@ -1,8 +1,11 @@
<?php
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\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\InstanceSettings;
use App\Models\Project;
@ -10,6 +13,7 @@
use App\Models\StandaloneDocker;
use App\Models\Team;
use App\Models\User;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
@ -169,6 +173,60 @@
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 ---
test('member cannot save application advanced settings', function () {

View file

@ -1,5 +1,6 @@
<?php
use App\Livewire\Project\Database\Heading;
use App\Models\InstanceSettings;
use App\Models\Project;
use App\Models\Server;
@ -10,11 +11,12 @@
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Livewire\Livewire;
uses(RefreshDatabase::class);
beforeEach(function () {
InstanceSettings::updateOrCreate(['id' => 0]);
InstanceSettings::unguarded(fn () => InstanceSettings::updateOrCreate(['id' => 0], ['id' => 0]));
$this->team = Team::factory()->create();
@ -89,6 +91,18 @@
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 ---
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\LogDrains;
use App\Livewire\Server\Navbar as ServerNavbar;
use App\Livewire\Server\ValidateAndInstall;
use App\Models\CloudProviderToken;
use App\Models\InstanceSettings;
use App\Models\Server;
@ -61,6 +62,24 @@
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 ---
test('admin can delete server', function () {