diff --git a/tests/Feature/Api/CloudProviderTokenApiTest.php b/tests/Feature/Api/CloudProviderTokenApiTest.php index 680586a9c0..a7b7f22cec 100644 --- a/tests/Feature/Api/CloudProviderTokenApiTest.php +++ b/tests/Feature/Api/CloudProviderTokenApiTest.php @@ -80,6 +80,27 @@ describe('GET /api/v1/cloud-tokens', function () { $response->assertJsonCount(1); }); + test('rejects a team member with a read token', function () { + CloudProviderToken::factory()->create([ + 'team_id' => $this->team->id, + 'name' => 'Production Hetzner', + 'provider' => 'hetzner', + ]); + + $member = User::factory()->create(); + $this->team->members()->attach($member->id, ['role' => 'member']); + session(['currentTeam' => $this->team]); + $memberToken = $member->createToken('member-read', ['read'])->plainTextToken; + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$memberToken, + 'Content-Type' => 'application/json', + ])->getJson('/api/v1/cloud-tokens'); + + $response->assertForbidden(); + expect($response->getContent())->not->toContain('Production Hetzner'); + }); + test('rejects request without authentication', function () { $response = $this->getJson('/api/v1/cloud-tokens'); $response->assertStatus(401); diff --git a/tests/Feature/Mcp/McpReadToolsTest.php b/tests/Feature/Mcp/McpReadToolsTest.php index 1337dc38d9..3be911b8c6 100644 --- a/tests/Feature/Mcp/McpReadToolsTest.php +++ b/tests/Feature/Mcp/McpReadToolsTest.php @@ -7,6 +7,7 @@ use App\Mcp\Tools\CoolifyHelp; use App\Models\Application; use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationPreview; +use App\Models\CloudProviderToken; use App\Models\Environment; use App\Models\EnvironmentVariable; use App\Models\GithubApp; @@ -903,6 +904,35 @@ test('list_applications list_databases list_services server_uuid filters use des ->not->toContain($otherServerService->uuid); }); +test('cloud_provider list_tokens rejects a team member with a read token', function () { + CloudProviderToken::factory()->create([ + 'team_id' => $this->team->id, + 'name' => 'Production Hetzner', + 'provider' => 'hetzner', + ]); + + $member = User::factory()->create(); + $this->team->members()->attach($member->id, ['role' => 'member']); + session(['currentTeam' => $this->team]); + $memberToken = $member->createToken('member-read', ['read'])->plainTextToken; + auth()->forgetGuards(); + + $response = $this->withHeaders([ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json, text/event-stream', + 'Authorization' => 'Bearer '.$memberToken, + ])->postJson('/mcp', [ + 'jsonrpc' => '2.0', + 'id' => 1, + 'method' => 'tools/call', + 'params' => ['name' => 'cloud_provider', 'arguments' => (object) ['action' => 'list_tokens']], + ]); + + $response->assertOk(); + expect($response->json('result.isError'))->toBeTrue() + ->and($response->getContent())->not->toContain('Production Hetzner'); +}); + test('list_tags and get_current_team and list_team_members are team scoped', function () { Tag::create(['name' => 'prod', 'team_id' => $this->team->id]); Tag::create(['name' => 'theirs', 'team_id' => Team::factory()->create()->id]);