From 6b1dcc0c96a918dde9d307e80f5ba6d9fd2a32c9 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 27 Oct 2026 14:38:24 +0100 Subject: [PATCH] fix(api): return success false on access denied responses Closes #12343 Co-Authored-By: Claude Opus 5.5 --- app/Http/Middleware/ApiAllowed.php | 2 +- app/Http/Middleware/ApiIpAllowed.php | 2 +- tests/Feature/ApiAccessDeniedResponseTest.php | 53 +++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 tests/Feature/ApiAccessDeniedResponseTest.php diff --git a/app/Http/Middleware/ApiAllowed.php b/app/Http/Middleware/ApiAllowed.php index 541bf3919a..7d6600bbb1 100644 --- a/app/Http/Middleware/ApiAllowed.php +++ b/app/Http/Middleware/ApiAllowed.php @@ -18,7 +18,7 @@ class ApiAllowed $isMcpRestBridge = $request->attributes->get('mcp_rest_bridge') === true; if (! $isMcpRestBridge && instanceSettings()->is_api_enabled === false) { - return response()->json(['success' => true, 'message' => 'API is disabled.'], 403); + return response()->json(['success' => false, 'message' => 'API is disabled.'], 403); } return app(ApiIpAllowed::class)->handle($request, $next); diff --git a/app/Http/Middleware/ApiIpAllowed.php b/app/Http/Middleware/ApiIpAllowed.php index 72aaa4b7c8..b37a9b38a7 100644 --- a/app/Http/Middleware/ApiIpAllowed.php +++ b/app/Http/Middleware/ApiIpAllowed.php @@ -23,7 +23,7 @@ class ApiIpAllowed $allowedIps = array_filter(array_map('trim', explode(',', $allowedIps))); if (! empty($allowedIps) && ! checkIPAgainstAllowlist(client_ip($request), $allowedIps)) { - return response()->json(['success' => true, 'message' => 'You are not allowed to access the API.'], 403); + return response()->json(['success' => false, 'message' => 'You are not allowed to access the API.'], 403); } return $next($request); diff --git a/tests/Feature/ApiAccessDeniedResponseTest.php b/tests/Feature/ApiAccessDeniedResponseTest.php new file mode 100644 index 0000000000..956d0491f1 --- /dev/null +++ b/tests/Feature/ApiAccessDeniedResponseTest.php @@ -0,0 +1,53 @@ +set('constants.coolify.self_hosted', true); + + InstanceSettings::forceCreate(['id' => 0, 'is_api_enabled' => true]); + + $team = Team::factory()->create(); + $user = User::factory()->create(); + $team->members()->attach($user->id, ['role' => 'owner']); + + session(['currentTeam' => $team]); + $this->token = $user->createToken('read', ['read'])->plainTextToken; +}); + +it('reports success false when the API is disabled', function () { + Once::flush(); + // Postgres returns a real boolean; SQLite would return 0 for the uncast column. + InstanceSettings::get()->is_api_enabled = false; + + $this->withToken($this->token) + ->getJson('/api/v1/version') + ->assertForbidden() + ->assertExactJson(['success' => false, 'message' => 'API is disabled.']); +}); + +it('reports success false when the client IP is not in the allow-list', function () { + InstanceSettings::query()->where('id', 0)->update(['allowed_ips' => '192.0.2.10']); + Once::flush(); + + $this->withToken($this->token) + ->getJson('/api/v1/version') + ->assertForbidden() + ->assertExactJson(['success' => false, 'message' => 'You are not allowed to access the API.']); +}); + +it('reports success false when the client IP is not in the allow-list on the IP-only route group', function () { + InstanceSettings::query()->where('id', 0)->update(['allowed_ips' => '192.0.2.10']); + Once::flush(); + + $this->withToken($this->token) + ->getJson('/api/v1/disable') + ->assertForbidden() + ->assertExactJson(['success' => false, 'message' => 'You are not allowed to access the API.']); +});