Files
coolify/tests/Feature/ScheduledTaskCrossTeamIdorTest.php
Andras BacsaiandClaude Opus 5.5 a5acf81751 fix(scheduled-tasks): stop 404 after deleting a scheduled task
The executions list polled with the task stored as a model, so a poll
after the delete failed to load it and showed a 404. Load the task by id
on each request and skip the refresh when it is gone. Return true from
delete so the confirmation modal does not refresh the deleted task.

Closes #11814

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 00:32:13 +02:00

126 lines
4.5 KiB
PHP

<?php
use App\Livewire\Project\Shared\ScheduledTask\Executions;
use App\Livewire\Project\Shared\ScheduledTask\Show;
use App\Models\Application;
use App\Models\Environment;
use App\Models\InstanceSettings;
use App\Models\Project;
use App\Models\ScheduledTask;
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Gate;
use Livewire\Attributes\Locked;
use Livewire\Livewire;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->withoutVite();
InstanceSettings::forceCreate(['id' => 0]);
$this->attacker = User::factory()->create();
$this->attackerTeam = Team::factory()->create();
$this->attacker->teams()->attach($this->attackerTeam, ['role' => 'owner']);
$this->victim = User::factory()->create();
$this->victimTeam = Team::factory()->create();
$this->victim->teams()->attach($this->victimTeam, ['role' => 'owner']);
// Attacker team gets a real server/project/env/app so Show can mount + authorize
$this->server = Server::factory()->create(['team_id' => $this->attackerTeam->id]);
$this->destination = StandaloneDocker::where('server_id', $this->server->id)->first();
$this->project = Project::factory()->create(['team_id' => $this->attackerTeam->id]);
$this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
$this->application = Application::factory()->create([
'environment_id' => $this->environment->id,
'destination_id' => $this->destination->id,
'destination_type' => $this->destination->getMorphClass(),
]);
$this->actingAs($this->attacker);
session(['currentTeam' => $this->attackerTeam]);
});
describe('ScheduledTask locked properties', function () {
test('Show component task property has Locked attribute', function () {
$property = new ReflectionProperty(Show::class, 'task');
$attributes = $property->getAttributes(Locked::class);
expect($attributes)->not->toBeEmpty();
});
test('Show component resource property has Locked attribute', function () {
$property = new ReflectionProperty(Show::class, 'resource');
$attributes = $property->getAttributes(Locked::class);
expect($attributes)->not->toBeEmpty();
});
test('Executions component task id property has Locked attribute', function () {
$property = new ReflectionProperty(Executions::class, 'taskId');
$attributes = $property->getAttributes(Locked::class);
expect($attributes)->not->toBeEmpty();
});
test('Executions component selected execution property has Locked attribute', function () {
$property = new ReflectionProperty(Executions::class, 'selectedExecution');
$attributes = $property->getAttributes(Locked::class);
expect($attributes)->not->toBeEmpty();
});
});
describe('ScheduledTask cross-team access', function () {
test('Executions rejects mounting another team task id', function () {
$victimTask = ScheduledTask::factory()->create([
'team_id' => $this->victimTeam->id,
'command' => 'echo top-secret-victim-command',
]);
Livewire::test(Executions::class, ['taskId' => $victimTask->id])
->assertStatus(404);
});
test('Show policy denies updating another team task', function () {
$victimTask = ScheduledTask::factory()->create([
'team_id' => $this->victimTeam->id,
'name' => 'victim-task',
'command' => 'echo original-victim-command',
]);
expect(
Gate::forUser($this->attacker)->allows('update', $victimTask)
)->toBeFalse();
});
test('Show policy denies deleting another team task', function () {
$victimTask = ScheduledTask::factory()->create([
'team_id' => $this->victimTeam->id,
'name' => 'victim-task',
'command' => 'echo original-victim-command',
]);
expect(
Gate::forUser($this->attacker)->allows('delete', $victimTask)
)->toBeFalse();
});
test('Show policy allows updating own team task', function () {
$ownTask = ScheduledTask::factory()->create([
'team_id' => $this->attackerTeam->id,
'application_id' => $this->application->id,
'name' => 'own-task',
'command' => 'echo original-command',
]);
expect(
Gate::forUser($this->attacker)->allows('update', $ownTask)
)->toBeTrue();
});
});