From 25b4236e356a796af65b778be6f0855cca4ec3ca Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 4 Nov 2026 11:32:42 +0100 Subject: [PATCH] fix(api): check deploy access before a deploy writes a Docker image preview The deploy endpoint now checks the deploy permission before it creates a preview or changes its image tag, not only before it queues the deployment. Co-Authored-By: Claude Opus 5.5 --- app/Http/Controllers/Api/DeployController.php | 5 +++ .../DockerImagePreviewDeploymentApiTest.php | 32 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php index 72bd5dd907..8c1db41d38 100644 --- a/app/Http/Controllers/Api/DeployController.php +++ b/app/Http/Controllers/Api/DeployController.php @@ -429,6 +429,11 @@ class DeployController extends Controller $preview = null; if ($pr !== 0) { if ($resource instanceof Application && $resource->build_pack === 'dockerimage') { + if (! auth()->user()?->can('deploy', $resource)) { + $deployments->push(['message' => 'Unauthorized to deploy this application.', 'resource_uuid' => $uuid]); + + continue; + } $preview = $this->upsertDockerImagePreview($resource, $pr, $dockerTag); $dockerTagForResource = $preview?->docker_registry_image_tag; } else { diff --git a/tests/Feature/DockerImagePreviewDeploymentApiTest.php b/tests/Feature/DockerImagePreviewDeploymentApiTest.php index 05945dc375..9c2dd0d43a 100644 --- a/tests/Feature/DockerImagePreviewDeploymentApiTest.php +++ b/tests/Feature/DockerImagePreviewDeploymentApiTest.php @@ -152,3 +152,35 @@ test('it rejects docker_tag for non docker image applications', function () { $response->assertSuccessful(); $response->assertJsonPath('deployments.0.message', 'docker_tag can only be used with Docker Image applications.'); }); + +test('a member without deploy access cannot create or change a docker image preview', function () { + $application = createDockerImageApplication($this->environment, $this->destination); + ApplicationPreview::create([ + 'application_id' => $application->id, + 'pull_request_id' => 7, + 'pull_request_html_url' => '', + 'docker_registry_image_tag' => 'pr_7', + ]); + + $otherProject = Project::factory()->create(['team_id' => $this->team->id]); + $member = User::factory()->create(); + $this->team->members()->attach($member->id, [ + 'role' => 'member', + 'access' => json_encode([ + ['scope' => 'team', 'levels' => ['read']], + ['scope' => 'project', 'uuid' => $otherProject->uuid, 'levels' => ['deploy']], + ]), + ]); + $token = $member->createToken('member-deploy', ['read', 'deploy'], null, $this->team->id)->plainTextToken; + + foreach ([[1234, 'pr_1234'], [7, 'attacker']] as [$pullRequestId, $dockerTag]) { + $this->withHeaders(['Authorization' => 'Bearer '.$token])->postJson('/api/v1/deploy', [ + 'uuid' => $application->uuid, + 'pull_request_id' => $pullRequestId, + 'docker_tag' => $dockerTag, + ])->assertForbidden(); + } + + expect($application->previews()->pluck('docker_registry_image_tag', 'pull_request_id')->all())->toBe([7 => 'pr_7']); + expect($application->deployment_queue()->count())->toBe(0); +});