diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index a7141756da..6d9fa1cec7 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -35,29 +35,80 @@ class DatabasesController extends Controller use Concerns\HandlesDatabaseImportsApi; use Concerns\HandlesTagsApi; - #[OA\Post(path: '/databases/{uuid}/imports/uploads', operationId: 'upload-database-import', summary: 'Upload database import', security: [['bearerAuth' => []]], tags: ['Databases'], responses: [new OA\Response(response: 201, description: 'Upload completed'), new OA\Response(response: 422, ref: '#/components/responses/422')])] + #[OA\Post( + path: '/databases/{uuid}/imports/uploads', + operationId: 'upload-database-import', + summary: 'Upload database import', + security: [['bearerAuth' => []]], + tags: ['Databases'], + parameters: [ + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'UUID of the database.', schema: new OA\Schema(type: 'string')), + ], + responses: [ + new OA\Response(response: 201, description: 'Upload completed'), + new OA\Response(response: 422, ref: '#/components/responses/422'), + ] + )] public function upload_import(Request $request, string $uuid): JsonResponse { $teamId = getTeamIdFromToken(); - $database = $teamId === null ? null : queryDatabaseByUuidWithinTeam($uuid, $teamId); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $database = queryDatabaseByUuidWithinTeam($uuid, $teamId); return $database ? $this->uploadDatabaseImport($request, $database, $teamId) : response()->json(['message' => 'Database not found.'], 404); } - #[OA\Post(path: '/databases/{uuid}/imports', operationId: 'create-database-import', summary: 'Import database backup', requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/DatabaseImportRequest')), security: [['bearerAuth' => []]], tags: ['Databases'], responses: [new OA\Response(response: 202, description: 'Import queued'), new OA\Response(response: 409, description: 'Import already active'), new OA\Response(response: 422, ref: '#/components/responses/422')])] + #[OA\Post( + path: '/databases/{uuid}/imports', + operationId: 'create-database-import', + summary: 'Import database backup', + requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/DatabaseImportRequest')), + security: [['bearerAuth' => []]], + tags: ['Databases'], + parameters: [ + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'UUID of the database.', schema: new OA\Schema(type: 'string')), + ], + responses: [ + new OA\Response(response: 202, description: 'Import queued'), + new OA\Response(response: 409, description: 'Import already active'), + new OA\Response(response: 422, ref: '#/components/responses/422'), + ] + )] public function create_import(Request $request, string $uuid): JsonResponse { $teamId = getTeamIdFromToken(); - $database = $teamId === null ? null : queryDatabaseByUuidWithinTeam($uuid, $teamId); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $database = queryDatabaseByUuidWithinTeam($uuid, $teamId); return $database ? $this->startDatabaseImport($request, $database, $teamId, 'api.databases.imports.show', ['uuid' => $uuid]) : response()->json(['message' => 'Database not found.'], 404); } - #[OA\Get(path: '/databases/{uuid}/imports/{activity_id}', operationId: 'get-database-import', summary: 'Get database import status', security: [['bearerAuth' => []]], tags: ['Databases'], responses: [new OA\Response(response: 200, description: 'Import status'), new OA\Response(response: 404, ref: '#/components/responses/404')])] + #[OA\Get( + path: '/databases/{uuid}/imports/{activity_id}', + operationId: 'get-database-import', + summary: 'Get database import status', + security: [['bearerAuth' => []]], + tags: ['Databases'], + parameters: [ + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'UUID of the database.', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'activity_id', in: 'path', required: true, description: 'Import activity ID.', schema: new OA\Schema(type: 'integer')), + ], + responses: [ + new OA\Response(response: 200, description: 'Import status', content: new OA\JsonContent(ref: '#/components/schemas/DatabaseImportStatus')), + new OA\Response(response: 404, ref: '#/components/responses/404'), + ] + )] public function show_import(Request $request, string $uuid, int $activity_id): JsonResponse { $teamId = getTeamIdFromToken(); - $database = $teamId === null ? null : queryDatabaseByUuidWithinTeam($uuid, $teamId); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $database = queryDatabaseByUuidWithinTeam($uuid, $teamId); return $database ? $this->showDatabaseImport($database, $teamId, $activity_id) : response()->json(['message' => 'Database not found.'], 404); } diff --git a/app/Http/Controllers/Api/ServiceDatabasesController.php b/app/Http/Controllers/Api/ServiceDatabasesController.php index 331b11944a..81e68ceb31 100644 --- a/app/Http/Controllers/Api/ServiceDatabasesController.php +++ b/app/Http/Controllers/Api/ServiceDatabasesController.php @@ -20,19 +20,64 @@ class ServiceDatabasesController extends Controller { use Concerns\HandlesDatabaseImportsApi; - #[OA\Post(path: '/services/{uuid}/databases/{database_uuid}/imports/uploads', operationId: 'upload-service-database-import', summary: 'Upload service database import', security: [['bearerAuth' => []]], tags: ['Service databases'], responses: [new OA\Response(response: 201, description: 'Upload completed'), new OA\Response(response: 422, ref: '#/components/responses/422')])] + #[OA\Post( + path: '/services/{uuid}/databases/{database_uuid}/imports/uploads', + operationId: 'upload-service-database-import', + summary: 'Upload service database import', + security: [['bearerAuth' => []]], + tags: ['Service databases'], + parameters: [ + new OA\Parameter(name: 'uuid', in: 'path', description: 'Service UUID.', required: true, schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'database_uuid', in: 'path', description: 'Service database UUID.', required: true, schema: new OA\Schema(type: 'string')), + ], + responses: [ + new OA\Response(response: 201, description: 'Upload completed'), + new OA\Response(response: 422, ref: '#/components/responses/422'), + ] + )] public function upload_import(Request $request): JsonResponse { return $this->withImportDatabase($request, fn (ServiceDatabase $database, int $teamId) => $this->uploadDatabaseImport($request, $database, $teamId)); } - #[OA\Post(path: '/services/{uuid}/databases/{database_uuid}/imports', operationId: 'create-service-database-import', summary: 'Import service database backup', requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/DatabaseImportRequest')), security: [['bearerAuth' => []]], tags: ['Service databases'], responses: [new OA\Response(response: 202, description: 'Import queued'), new OA\Response(response: 409, description: 'Import already active'), new OA\Response(response: 422, ref: '#/components/responses/422')])] + #[OA\Post( + path: '/services/{uuid}/databases/{database_uuid}/imports', + operationId: 'create-service-database-import', + summary: 'Import service database backup', + requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/DatabaseImportRequest')), + security: [['bearerAuth' => []]], + tags: ['Service databases'], + parameters: [ + new OA\Parameter(name: 'uuid', in: 'path', description: 'Service UUID.', required: true, schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'database_uuid', in: 'path', description: 'Service database UUID.', required: true, schema: new OA\Schema(type: 'string')), + ], + responses: [ + new OA\Response(response: 202, description: 'Import queued'), + new OA\Response(response: 409, description: 'Import already active'), + new OA\Response(response: 422, ref: '#/components/responses/422'), + ] + )] public function create_import(Request $request): JsonResponse { return $this->withImportDatabase($request, fn (ServiceDatabase $database, int $teamId) => $this->startDatabaseImport($request, $database, $teamId, 'api.service-databases.imports.show', ['uuid' => $request->route('uuid'), 'database_uuid' => $database->uuid])); } - #[OA\Get(path: '/services/{uuid}/databases/{database_uuid}/imports/{activity_id}', operationId: 'get-service-database-import', summary: 'Get service database import status', security: [['bearerAuth' => []]], tags: ['Service databases'], responses: [new OA\Response(response: 200, description: 'Import status'), new OA\Response(response: 404, ref: '#/components/responses/404')])] + #[OA\Get( + path: '/services/{uuid}/databases/{database_uuid}/imports/{activity_id}', + operationId: 'get-service-database-import', + summary: 'Get service database import status', + security: [['bearerAuth' => []]], + tags: ['Service databases'], + parameters: [ + new OA\Parameter(name: 'uuid', in: 'path', description: 'Service UUID.', required: true, schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'database_uuid', in: 'path', description: 'Service database UUID.', required: true, schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'activity_id', in: 'path', description: 'Import activity ID.', required: true, schema: new OA\Schema(type: 'integer')), + ], + responses: [ + new OA\Response(response: 200, description: 'Import status', content: new OA\JsonContent(ref: '#/components/schemas/DatabaseImportStatus')), + new OA\Response(response: 404, ref: '#/components/responses/404'), + ] + )] public function show_import(Request $request): JsonResponse { return $this->withImportDatabase($request, fn (ServiceDatabase $database, int $teamId) => $this->showDatabaseImport($database, $teamId, (int) $request->route('activity_id'))); @@ -41,7 +86,11 @@ class ServiceDatabasesController extends Controller private function withImportDatabase(Request $request, callable $callback): JsonResponse { $teamId = getTeamIdFromToken(); - $service = $teamId === null ? null : $this->resolveService($request, $teamId); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $service = $this->resolveService($request, $teamId); $database = $service ? $this->resolveServiceDatabase($request, $service) : null; return $database ? $callback($database, $teamId) : response()->json(['message' => 'Service database not found.'], 404); diff --git a/tests/Feature/Api/DatabaseImportApiTest.php b/tests/Feature/Api/DatabaseImportApiTest.php index fc1db1333e..abd0626af4 100644 --- a/tests/Feature/Api/DatabaseImportApiTest.php +++ b/tests/Feature/Api/DatabaseImportApiTest.php @@ -1,6 +1,8 @@ save(); $this->withHeaders($this->headers)->getJson("/api/v1/databases/{$database->uuid}/imports/{$activity->id}")->assertNotFound(); }); + +test('returns invalid token when the access token team is not a member team', function (string $method, string $path) { + $this->withoutMiddleware([ + EnsureTokenBelongsToCurrentTeamMember::class, + ApiAbility::class, + ]); + + $database = StandalonePostgresql::create(['uuid' => (string) Str::uuid(), 'name' => 'db', 'postgres_user' => 'postgres', 'postgres_password' => 'password', 'postgres_db' => 'db', 'image' => 'postgres:17', 'status' => 'running', 'environment_id' => $this->environment->id, 'destination_id' => $this->destination->id, 'destination_type' => $this->destination->getMorphClass()]); + $foreignTeam = Team::factory()->create(); + $plainTextToken = 'no-team'; + $token = $this->user->tokens()->create([ + 'name' => 'imports-foreign-team', + 'token' => hash('sha256', $plainTextToken), + 'abilities' => ['deploy', 'read'], + 'team_id' => $foreignTeam->id, + ]); + + $this->withHeaders(['Authorization' => 'Bearer '.$token->id.'|'.$plainTextToken]) + ->{$method}(sprintf($path, $database->uuid)) + ->assertBadRequest() + ->assertJson([ + 'message' => 'Invalid token.', + 'docs' => 'https://coolify.io/docs/api-reference/authorization', + ]); +})->with([ + 'upload' => ['postJson', '/api/v1/databases/%s/imports/uploads'], + 'create' => ['postJson', '/api/v1/databases/%s/imports'], + 'show' => ['getJson', '/api/v1/databases/%s/imports/1'], +]); diff --git a/tests/Feature/Api/ServiceDatabaseImportApiTest.php b/tests/Feature/Api/ServiceDatabaseImportApiTest.php index ae05455f5e..df591e97cb 100644 --- a/tests/Feature/Api/ServiceDatabaseImportApiTest.php +++ b/tests/Feature/Api/ServiceDatabaseImportApiTest.php @@ -1,9 +1,13 @@ environment = Environment::factory()->create(['project_id' => $this->project->id]); }); -use App\Models\Service; -use App\Models\ServiceDatabase; - test('validates service database imports and binds database to service', function () { $service = Service::factory()->create(['environment_id' => $this->environment->id, 'server_id' => $this->server->id, 'destination_id' => $this->destination->id, 'destination_type' => $this->destination->getMorphClass(), 'docker_compose_raw' => "services:\n postgres:\n image: postgres:17\n"]); $database = ServiceDatabase::create(['uuid' => (string) Str::uuid(), 'name' => 'postgres', 'service_id' => $service->id, 'image' => 'postgres:17']); @@ -40,3 +41,33 @@ test('validates service database imports and binds database to service', functio $otherService = Service::factory()->create(['environment_id' => $this->environment->id, 'server_id' => $this->server->id, 'destination_id' => $this->destination->id, 'destination_type' => $this->destination->getMorphClass(), 'docker_compose_raw' => "services: {}\n"]); $this->withHeaders($this->headers)->postJson("/api/v1/services/{$otherService->uuid}/databases/{$database->uuid}/imports", ['source' => 'server', 'path' => '/tmp/a.sql'])->assertNotFound(); }); + +test('returns invalid token when the access token team is not a member team', function (string $method, string $suffix) { + $this->withoutMiddleware([ + EnsureTokenBelongsToCurrentTeamMember::class, + ApiAbility::class, + ]); + + $service = Service::factory()->create(['environment_id' => $this->environment->id, 'server_id' => $this->server->id, 'destination_id' => $this->destination->id, 'destination_type' => $this->destination->getMorphClass(), 'docker_compose_raw' => "services:\n postgres:\n image: postgres:17\n"]); + $database = ServiceDatabase::create(['uuid' => (string) Str::uuid(), 'name' => 'postgres', 'service_id' => $service->id, 'image' => 'postgres:17']); + $foreignTeam = Team::factory()->create(); + $plainTextToken = 'no-team'; + $token = $this->user->tokens()->create([ + 'name' => 'imports-foreign-team', + 'token' => hash('sha256', $plainTextToken), + 'abilities' => ['deploy', 'read'], + 'team_id' => $foreignTeam->id, + ]); + + $this->withHeaders(['Authorization' => 'Bearer '.$token->id.'|'.$plainTextToken]) + ->{$method}("/api/v1/services/{$service->uuid}/databases/{$database->uuid}/{$suffix}") + ->assertBadRequest() + ->assertJson([ + 'message' => 'Invalid token.', + 'docs' => 'https://coolify.io/docs/api-reference/authorization', + ]); +})->with([ + 'upload' => ['postJson', 'imports/uploads'], + 'create' => ['postJson', 'imports'], + 'show' => ['getJson', 'imports/1'], +]);