Strip advisory identifiers (GHSA-*) from describe blocks, test docblocks, and inline comments. Replace with plain descriptive labels. Also clean up FQCNs to use imported class names and minor style fixes (string concatenation spacing).
186 lines
7.5 KiB
PHP
186 lines
7.5 KiB
PHP
<?php
|
|
|
|
use App\Enums\ApplicationDeploymentStatus;
|
|
use App\Livewire\Boarding\Index as BoardingIndex;
|
|
use App\Livewire\GlobalSearch;
|
|
use App\Livewire\Project\CloneMe;
|
|
use App\Livewire\Project\DeleteProject;
|
|
use App\Models\Application;
|
|
use App\Models\ApplicationDeploymentQueue;
|
|
use App\Models\Environment;
|
|
use App\Models\Project;
|
|
use App\Models\Server;
|
|
use App\Models\StandaloneDocker;
|
|
use App\Models\Team;
|
|
use App\Models\User;
|
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Livewire\Livewire;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
// Attacker: Team A
|
|
$this->userA = User::factory()->create();
|
|
$this->teamA = Team::factory()->create();
|
|
$this->userA->teams()->attach($this->teamA, ['role' => 'owner']);
|
|
|
|
$this->serverA = Server::factory()->create(['team_id' => $this->teamA->id]);
|
|
$this->projectA = Project::factory()->create(['team_id' => $this->teamA->id]);
|
|
$this->environmentA = Environment::factory()->create(['project_id' => $this->projectA->id]);
|
|
|
|
// Victim: Team B
|
|
$this->userB = User::factory()->create();
|
|
$this->teamB = Team::factory()->create();
|
|
$this->userB->teams()->attach($this->teamB, ['role' => 'owner']);
|
|
|
|
$this->serverB = Server::factory()->create(['team_id' => $this->teamB->id]);
|
|
$this->projectB = Project::factory()->create(['team_id' => $this->teamB->id]);
|
|
$this->environmentB = Environment::factory()->create(['project_id' => $this->projectB->id]);
|
|
|
|
// Act as attacker (Team A)
|
|
$this->actingAs($this->userA);
|
|
session(['currentTeam' => $this->teamA]);
|
|
});
|
|
|
|
describe('Boarding Server IDOR', function () {
|
|
test('boarding mount cannot load server from another team via selectedExistingServer', function () {
|
|
$component = Livewire::test(BoardingIndex::class, [
|
|
'selectedServerType' => 'remote',
|
|
'selectedExistingServer' => $this->serverB->id,
|
|
]);
|
|
|
|
// The server from Team B should NOT be loaded
|
|
expect($component->get('createdServer'))->toBeNull();
|
|
});
|
|
|
|
test('boarding mount can load own team server via selectedExistingServer', function () {
|
|
$component = Livewire::test(BoardingIndex::class, [
|
|
'selectedServerType' => 'remote',
|
|
'selectedExistingServer' => $this->serverA->id,
|
|
]);
|
|
|
|
// Own team server should load successfully
|
|
expect($component->get('createdServer'))->not->toBeNull();
|
|
expect($component->get('createdServer')->id)->toBe($this->serverA->id);
|
|
});
|
|
});
|
|
|
|
describe('Boarding Project IDOR', function () {
|
|
test('boarding mount cannot load project from another team via selectedProject', function () {
|
|
$component = Livewire::test(BoardingIndex::class, [
|
|
'selectedProject' => $this->projectB->id,
|
|
]);
|
|
|
|
// The project from Team B should NOT be loaded
|
|
expect($component->get('createdProject'))->toBeNull();
|
|
});
|
|
|
|
test('boarding selectExistingProject cannot load project from another team', function () {
|
|
$component = Livewire::test(BoardingIndex::class)
|
|
->set('selectedProject', $this->projectB->id)
|
|
->call('selectExistingProject');
|
|
|
|
expect($component->get('createdProject'))->toBeNull();
|
|
$component->assertDispatched('error');
|
|
});
|
|
|
|
test('boarding selectExistingProject can load own team project', function () {
|
|
$component = Livewire::test(BoardingIndex::class)
|
|
->set('selectedProject', $this->projectA->id)
|
|
->call('selectExistingProject');
|
|
|
|
expect($component->get('createdProject'))->not->toBeNull();
|
|
expect($component->get('createdProject')->id)->toBe($this->projectA->id);
|
|
});
|
|
});
|
|
|
|
describe('GlobalSearch Server IDOR', function () {
|
|
test('loadDestinations cannot access server from another team', function () {
|
|
$component = Livewire::test(GlobalSearch::class)
|
|
->set('selectedServerId', $this->serverB->id)
|
|
->call('loadDestinations');
|
|
|
|
// Should dispatch error because server is not found (team-scoped)
|
|
$component->assertDispatched('error');
|
|
});
|
|
});
|
|
|
|
describe('GlobalSearch Project IDOR', function () {
|
|
test('loadEnvironments cannot access project from another team', function () {
|
|
$component = Livewire::test(GlobalSearch::class)
|
|
->set('selectedProjectUuid', $this->projectB->uuid)
|
|
->call('loadEnvironments');
|
|
|
|
// Should not load environments from another team's project
|
|
expect($component->get('availableEnvironments'))->toBeEmpty();
|
|
});
|
|
});
|
|
|
|
describe('DeleteProject IDOR', function () {
|
|
test('cannot mount DeleteProject with project from another team', function () {
|
|
// Should throw ModelNotFoundException (404) because team-scoped query won't find it
|
|
Livewire::test(DeleteProject::class, ['project_id' => $this->projectB->id]);
|
|
})->throws(ModelNotFoundException::class);
|
|
|
|
test('can mount DeleteProject with own team project', function () {
|
|
$component = Livewire::test(DeleteProject::class, ['project_id' => $this->projectA->id]);
|
|
|
|
expect($component->get('projectName'))->toBe($this->projectA->name);
|
|
});
|
|
});
|
|
|
|
describe('CloneMe Project IDOR', function () {
|
|
test('cannot mount CloneMe with project UUID from another team', function () {
|
|
// Should throw ModelNotFoundException because team-scoped query won't find it
|
|
Livewire::test(CloneMe::class, [
|
|
'project_uuid' => $this->projectB->uuid,
|
|
'environment_uuid' => $this->environmentB->uuid,
|
|
]);
|
|
})->throws(ModelNotFoundException::class);
|
|
|
|
test('can mount CloneMe with own team project UUID', function () {
|
|
$component = Livewire::test(CloneMe::class, [
|
|
'project_uuid' => $this->projectA->uuid,
|
|
'environment_uuid' => $this->environmentA->uuid,
|
|
]);
|
|
|
|
expect($component->get('project_id'))->toBe($this->projectA->id);
|
|
});
|
|
});
|
|
|
|
describe('DeployController API Server IDOR', function () {
|
|
test('deploy cancel API cannot access build server from another team', function () {
|
|
// Create a deployment queue entry that references Team B's server as build_server
|
|
$application = Application::factory()->create([
|
|
'environment_id' => $this->environmentA->id,
|
|
'destination_id' => StandaloneDocker::factory()->create(['server_id' => $this->serverA->id])->id,
|
|
'destination_type' => StandaloneDocker::class,
|
|
]);
|
|
|
|
$deployment = ApplicationDeploymentQueue::create([
|
|
'application_id' => $application->id,
|
|
'deployment_uuid' => 'test-deploy-'.fake()->uuid(),
|
|
'server_id' => $this->serverA->id,
|
|
'build_server_id' => $this->serverB->id, // Cross-team build server
|
|
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
|
|
]);
|
|
|
|
$token = $this->userA->createToken('test-token', ['*']);
|
|
|
|
$response = $this->withHeaders([
|
|
'Authorization' => 'Bearer '.$token->plainTextToken,
|
|
])->deleteJson("/api/v1/deployments/{$deployment->deployment_uuid}");
|
|
|
|
// The cancellation should proceed but the build_server should NOT be found
|
|
// (team-scoped query returns null for Team B's server)
|
|
// The deployment gets cancelled but no remote process runs on the wrong server
|
|
$response->assertOk();
|
|
|
|
// Verify the deployment was cancelled
|
|
$deployment->refresh();
|
|
expect($deployment->status)->toBe(
|
|
ApplicationDeploymentStatus::CANCELLED_BY_USER->value
|
|
);
|
|
});
|
|
});
|