diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php index 64bf26c1bb..eb137c5349 100644 --- a/app/Http/Controllers/Api/ProjectController.php +++ b/app/Http/Controllers/Api/ProjectController.php @@ -158,6 +158,8 @@ class ProjectController extends Controller if (! $project) { return response()->json(['message' => 'Project not found.'], 404); } + $this->authorize('view', $project); + $environment = $project->environments()->whereName($request->environment_name_or_uuid)->first(); if (! $environment) { $environment = $project->environments()->whereUuid($request->environment_name_or_uuid)->first(); diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php index d50a5226a9..f7966c71f1 100644 --- a/app/Http/Controllers/Api/ServersController.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -550,11 +550,7 @@ class ServersController extends Controller } $foundServer = ModelsServer::whereIp($request->ip)->first(); if ($foundServer) { - if ($foundServer->team_id === $teamId) { - return response()->json(['message' => 'A server with this IP/Domain already exists in your team.'], 400); - } - - return response()->json(['message' => 'A server with this IP/Domain is already in use by another team.'], 400); + return response()->json(['message' => 'A server with this IP/Domain is already in use.'], 400); } $proxyType = $request->proxy_type ? str($request->proxy_type)->upper() : ProxyTypes::TRAEFIK->value; diff --git a/app/Livewire/Project/Shared/ScheduledTask/Add.php b/app/Livewire/Project/Shared/ScheduledTask/Add.php index 2d6b76c25f..61bc6b0fbc 100644 --- a/app/Livewire/Project/Shared/ScheduledTask/Add.php +++ b/app/Livewire/Project/Shared/ScheduledTask/Add.php @@ -2,7 +2,10 @@ namespace App\Livewire\Project\Shared\ScheduledTask; +use App\Models\Application; use App\Models\ScheduledTask; +use App\Models\Service; +use App\Models\StandalonePostgresql; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Support\Collection; use Livewire\Attributes\Locked; @@ -59,13 +62,13 @@ class Add extends Component // Get the resource based on type and id switch ($this->type) { case 'application': - $this->resource = \App\Models\Application::findOrFail($this->id); + $this->resource = Application::ownedByCurrentTeam()->findOrFail($this->id); break; case 'service': - $this->resource = \App\Models\Service::findOrFail($this->id); + $this->resource = Service::ownedByCurrentTeam()->findOrFail($this->id); break; case 'standalone-postgresql': - $this->resource = \App\Models\StandalonePostgresql::findOrFail($this->id); + $this->resource = StandalonePostgresql::ownedByCurrentTeam()->findOrFail($this->id); break; default: throw new \Exception('Invalid resource type'); diff --git a/app/Livewire/Server/DockerCleanupExecutions.php b/app/Livewire/Server/DockerCleanupExecutions.php index 56d6130644..6a739bc84c 100644 --- a/app/Livewire/Server/DockerCleanupExecutions.php +++ b/app/Livewire/Server/DockerCleanupExecutions.php @@ -2,7 +2,6 @@ namespace App\Livewire\Server; -use App\Models\DockerCleanupExecution; use App\Models\Server; use Illuminate\Support\Collection; use Livewire\Component; @@ -46,7 +45,7 @@ class DockerCleanupExecutions extends Component ->get(); if ($this->selectedKey) { - $this->selectedExecution = DockerCleanupExecution::find($this->selectedKey); + $this->selectedExecution = $this->server->dockerCleanupExecutions()->find($this->selectedKey); if ($this->selectedExecution && $this->selectedExecution->status !== 'running') { $this->isPollingActive = false; } @@ -64,7 +63,7 @@ class DockerCleanupExecutions extends Component return; } $this->selectedKey = $key; - $this->selectedExecution = DockerCleanupExecution::find($key); + $this->selectedExecution = $this->server->dockerCleanupExecutions()->find($key); $this->currentPage = 1; if ($this->selectedExecution && $this->selectedExecution->status === 'running') { diff --git a/tests/Feature/ResourceAccessConsistencyTest.php b/tests/Feature/ResourceAccessConsistencyTest.php new file mode 100644 index 0000000000..2511e14342 --- /dev/null +++ b/tests/Feature/ResourceAccessConsistencyTest.php @@ -0,0 +1,120 @@ + 'file']); + + InstanceSettings::forceCreate(['id' => 0, 'is_api_enabled' => true]); + + $this->team = Team::factory()->create(); + $this->user = User::factory()->create(); + $this->user->teams()->attach($this->team, ['role' => 'owner']); + + $this->otherTeam = Team::factory()->create(); + + session(['currentTeam' => $this->team]); + + $this->privateKey = PrivateKey::withoutEvents(fn () => PrivateKey::forceCreate([ + 'uuid' => (string) Str::uuid(), + 'name' => 'IDOR test key', + 'private_key' => 'test-private-key', + 'team_id' => $this->team->id, + ])); + + $token = $this->user->createToken('idor-hardening', ['*']); + $token->accessToken->forceFill(['team_id' => $this->team->id])->save(); + $this->token = $token->plainTextToken; +}); + +test('server creation returns a consistent duplicate address response', function () { + $ownServer = Server::factory()->create([ + 'ip' => '192.0.2.10', + 'team_id' => $this->team->id, + ]); + $otherServer = Server::factory()->create([ + 'ip' => '192.0.2.20', + 'team_id' => $this->otherTeam->id, + ]); + + $payload = fn (Server $server): array => [ + 'name' => 'Duplicate server', + 'ip' => $server->ip, + 'private_key_uuid' => $this->privateKey->uuid, + 'user' => 'root', + ]; + + $ownResponse = $this->withToken($this->token)->postJson('/api/v1/servers', $payload($ownServer)); + $otherResponse = $this->withToken($this->token)->postJson('/api/v1/servers', $payload($otherServer)); + + $ownResponse->assertBadRequest(); + $otherResponse->assertBadRequest(); + expect($ownResponse->json('message')) + ->toBe('A server with this IP/Domain is already in use.') + ->toBe($otherResponse->json('message')); +}); + +test('environment details applies the project view policy', function () { + $project = Project::factory()->create(['team_id' => $this->team->id]); + $environment = Environment::factory()->create(['project_id' => $project->id]); + + Gate::before(fn (User $user, string $ability): ?bool => $ability === 'view' ? false : null); + + $this->withToken($this->token) + ->getJson("/api/v1/projects/{$project->uuid}/{$environment->uuid}") + ->assertForbidden(); +}); + +test('docker cleanup execution selection only uses the mounted server', function () { + $this->actingAs($this->user); + + $server = Server::factory()->create(['team_id' => $this->team->id]); + $otherServer = Server::factory()->create(['team_id' => $this->otherTeam->id]); + $otherExecution = DockerCleanupExecution::create([ + 'server_id' => $otherServer->id, + 'status' => 'success', + 'message' => 'other team cleanup output', + ]); + + Livewire::test(DockerCleanupExecutions::class, ['server' => $server]) + ->call('selectExecution', $otherExecution->id) + ->assertSet('selectedExecution', null); +}); + +test('scheduled task form only mounts applications from the current team', function () { + $this->actingAs($this->user); + + $server = Server::factory()->create(['team_id' => $this->otherTeam->id]); + $destination = StandaloneDocker::where('server_id', $server->id)->firstOrFail(); + $project = Project::factory()->create(['team_id' => $this->otherTeam->id]); + $environment = Environment::factory()->create(['project_id' => $project->id]); + $application = Application::factory()->create([ + 'environment_id' => $environment->id, + 'destination_id' => $destination->id, + 'destination_type' => $destination->getMorphClass(), + ]); + + Livewire::test(Add::class, [ + 'id' => (string) $application->id, + 'type' => 'application', + 'containerNames' => collect(), + ]); +})->throws(ModelNotFoundException::class);