From 802569bf636b8172385981e8a52e312745f826cc Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 16 Oct 2025 13:19:05 +0200 Subject: [PATCH] Changes auto-committed by Conductor --- app/Http/Controllers/Api/GithubController.php | 82 +++++++ routes/api.php | 1 + tests/Feature/GithubAppsListApiTest.php | 222 ++++++++++++++++++ 3 files changed, 305 insertions(+) create mode 100644 tests/Feature/GithubAppsListApiTest.php diff --git a/app/Http/Controllers/Api/GithubController.php b/app/Http/Controllers/Api/GithubController.php index 7ddbaf991..f6a6b3513 100644 --- a/app/Http/Controllers/Api/GithubController.php +++ b/app/Http/Controllers/Api/GithubController.php @@ -12,6 +12,88 @@ use OpenApi\Attributes as OA; class GithubController extends Controller { + private function removeSensitiveData($githubApp) + { + $githubApp->makeHidden([ + 'client_secret', + 'webhook_secret', + ]); + + return serializeApiResponse($githubApp); + } + + #[OA\Get( + summary: 'List', + description: 'List all GitHub apps.', + path: '/github-apps', + operationId: 'list-github-apps', + security: [ + ['bearerAuth' => []], + ], + tags: ['GitHub Apps'], + responses: [ + new OA\Response( + response: 200, + description: 'List of GitHub apps.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'array', + items: new OA\Items( + type: 'object', + properties: [ + 'id' => ['type' => 'integer'], + 'uuid' => ['type' => 'string'], + 'name' => ['type' => 'string'], + 'organization' => ['type' => 'string', 'nullable' => true], + 'api_url' => ['type' => 'string'], + 'html_url' => ['type' => 'string'], + 'custom_user' => ['type' => 'string'], + 'custom_port' => ['type' => 'integer'], + 'app_id' => ['type' => 'integer'], + 'installation_id' => ['type' => 'integer'], + 'client_id' => ['type' => 'string'], + 'private_key_id' => ['type' => 'integer'], + 'is_system_wide' => ['type' => 'boolean'], + 'is_public' => ['type' => 'boolean'], + 'team_id' => ['type' => 'integer'], + 'type' => ['type' => 'string'], + ] + ) + ) + ), + ] + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function list_github_apps(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $githubApps = GithubApp::where(function ($query) use ($teamId) { + $query->where('team_id', $teamId) + ->orWhere('is_system_wide', true); + })->get(); + + $githubApps = $githubApps->map(function ($app) { + return $this->removeSensitiveData($app); + }); + + return response()->json($githubApps); + } + #[OA\Post( summary: 'Create GitHub App', description: 'Create a new GitHub app.', diff --git a/routes/api.php b/routes/api.php index d23a4b2b1..366a97d74 100644 --- a/routes/api.php +++ b/routes/api.php @@ -105,6 +105,7 @@ Route::group([ Route::match(['get', 'post'], '/applications/{uuid}/restart', [ApplicationsController::class, 'action_restart'])->middleware(['api.ability:write']); Route::match(['get', 'post'], '/applications/{uuid}/stop', [ApplicationsController::class, 'action_stop'])->middleware(['api.ability:write']); + Route::get('/github-apps', [GithubController::class, 'list_github_apps'])->middleware(['api.ability:read']); Route::post('/github-apps', [GithubController::class, 'create_github_app'])->middleware(['api.ability:write']); Route::patch('/github-apps/{github_app_id}', [GithubController::class, 'update_github_app'])->middleware(['api.ability:write']); Route::delete('/github-apps/{github_app_id}', [GithubController::class, 'delete_github_app'])->middleware(['api.ability:write']); diff --git a/tests/Feature/GithubAppsListApiTest.php b/tests/Feature/GithubAppsListApiTest.php new file mode 100644 index 000000000..a6ce59dca --- /dev/null +++ b/tests/Feature/GithubAppsListApiTest.php @@ -0,0 +1,222 @@ +team = Team::factory()->create(); + $this->user = User::factory()->create(); + $this->team->members()->attach($this->user->id, ['role' => 'owner']); + + // Create an API token for the user + $this->token = $this->user->createToken('test-token', ['*'], $this->team->id); + $this->bearerToken = $this->token->plainTextToken; + + // Create a private key for the team + $this->privateKey = PrivateKey::create([ + 'name' => 'Test Key', + 'private_key' => 'test-private-key-content', + 'team_id' => $this->team->id, + ]); +}); + +describe('GET /api/v1/github-apps', function () { + test('returns 401 when not authenticated', function () { + $response = $this->getJson('/api/v1/github-apps'); + + $response->assertStatus(401); + }); + + test('returns empty array when no github apps exist', function () { + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + ])->getJson('/api/v1/github-apps'); + + $response->assertStatus(200); + $response->assertJson([]); + }); + + test('returns team github apps', function () { + // Create a GitHub app for the team + $githubApp = GithubApp::create([ + 'name' => 'Test GitHub App', + 'api_url' => 'https://api.github.com', + 'html_url' => 'https://github.com', + 'app_id' => 12345, + 'installation_id' => 67890, + 'client_id' => 'test-client-id', + 'client_secret' => 'test-client-secret', + 'webhook_secret' => 'test-webhook-secret', + 'private_key_id' => $this->privateKey->id, + 'team_id' => $this->team->id, + 'is_system_wide' => false, + 'is_public' => false, + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + ])->getJson('/api/v1/github-apps'); + + $response->assertStatus(200); + $response->assertJsonCount(1); + $response->assertJsonFragment([ + 'name' => 'Test GitHub App', + 'app_id' => 12345, + ]); + }); + + test('does not return sensitive data', function () { + // Create a GitHub app + GithubApp::create([ + 'name' => 'Test GitHub App', + 'api_url' => 'https://api.github.com', + 'html_url' => 'https://github.com', + 'app_id' => 12345, + 'installation_id' => 67890, + 'client_id' => 'test-client-id', + 'client_secret' => 'secret-should-be-hidden', + 'webhook_secret' => 'webhook-secret-should-be-hidden', + 'private_key_id' => $this->privateKey->id, + 'team_id' => $this->team->id, + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + ])->getJson('/api/v1/github-apps'); + + $response->assertStatus(200); + $json = $response->json(); + + // Ensure sensitive data is not present + expect($json[0])->not->toHaveKey('client_secret'); + expect($json[0])->not->toHaveKey('webhook_secret'); + }); + + test('returns system-wide github apps', function () { + // Create a system-wide GitHub app + $systemApp = GithubApp::create([ + 'name' => 'System GitHub App', + 'api_url' => 'https://api.github.com', + 'html_url' => 'https://github.com', + 'app_id' => 11111, + 'installation_id' => 22222, + 'client_id' => 'system-client-id', + 'client_secret' => 'system-secret', + 'webhook_secret' => 'system-webhook', + 'private_key_id' => $this->privateKey->id, + 'team_id' => $this->team->id, + 'is_system_wide' => true, + ]); + + // Create another team and user + $otherTeam = Team::factory()->create(); + $otherUser = User::factory()->create(); + $otherTeam->members()->attach($otherUser->id, ['role' => 'owner']); + $otherToken = $otherUser->createToken('other-token', ['*'], $otherTeam->id); + + // System-wide apps should be visible to other teams + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$otherToken->plainTextToken, + ])->getJson('/api/v1/github-apps'); + + $response->assertStatus(200); + $response->assertJsonFragment([ + 'name' => 'System GitHub App', + 'is_system_wide' => true, + ]); + }); + + test('does not return other teams github apps', function () { + // Create a GitHub app for this team + GithubApp::create([ + 'name' => 'Team 1 App', + 'api_url' => 'https://api.github.com', + 'html_url' => 'https://github.com', + 'app_id' => 11111, + 'installation_id' => 22222, + 'client_id' => 'team1-client-id', + 'client_secret' => 'team1-secret', + 'webhook_secret' => 'team1-webhook', + 'private_key_id' => $this->privateKey->id, + 'team_id' => $this->team->id, + 'is_system_wide' => false, + ]); + + // Create another team with a GitHub app + $otherTeam = Team::factory()->create(); + $otherPrivateKey = PrivateKey::create([ + 'name' => 'Other Key', + 'private_key' => 'other-key', + 'team_id' => $otherTeam->id, + ]); + GithubApp::create([ + 'name' => 'Team 2 App', + 'api_url' => 'https://api.github.com', + 'html_url' => 'https://github.com', + 'app_id' => 33333, + 'installation_id' => 44444, + 'client_id' => 'team2-client-id', + 'client_secret' => 'team2-secret', + 'webhook_secret' => 'team2-webhook', + 'private_key_id' => $otherPrivateKey->id, + 'team_id' => $otherTeam->id, + 'is_system_wide' => false, + ]); + + // Request from first team should only see their app + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + ])->getJson('/api/v1/github-apps'); + + $response->assertStatus(200); + $response->assertJsonCount(1); + $response->assertJsonFragment(['name' => 'Team 1 App']); + $response->assertJsonMissing(['name' => 'Team 2 App']); + }); + + test('returns correct response structure', function () { + GithubApp::create([ + 'name' => 'Test App', + 'api_url' => 'https://api.github.com', + 'html_url' => 'https://github.com', + 'custom_user' => 'git', + 'custom_port' => 22, + 'app_id' => 12345, + 'installation_id' => 67890, + 'client_id' => 'client-id', + 'client_secret' => 'secret', + 'webhook_secret' => 'webhook', + 'private_key_id' => $this->privateKey->id, + 'team_id' => $this->team->id, + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + ])->getJson('/api/v1/github-apps'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + [ + 'id', + 'uuid', + 'name', + 'api_url', + 'html_url', + 'custom_user', + 'custom_port', + 'app_id', + 'installation_id', + 'client_id', + 'private_key_id', + 'team_id', + 'type', + ], + ]); + }); +});