fix(api): return invalid token for database import endpoints

Import upload/create/show now return invalidTokenResponse() when the
access token has no team, matching the rest of the database API.
This commit is contained in:
Andras Bacsai
2026-09-09 13:09:31 +02:00
parent 27bb601e66
commit b5ca99c69d
4 changed files with 175 additions and 13 deletions
@@ -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);
}
@@ -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);
@@ -1,6 +1,8 @@
<?php
use App\Actions\Database\StartDatabaseImport;
use App\Http\Middleware\ApiAbility;
use App\Http\Middleware\EnsureTokenBelongsToCurrentTeamMember;
use App\Models\AuditEvent;
use App\Models\Environment;
use App\Models\InstanceSettings;
@@ -131,3 +133,32 @@ test('returns only a team and resource scoped import activity', function () {
$activity->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'],
]);
@@ -1,9 +1,13 @@
<?php
use App\Http\Middleware\ApiAbility;
use App\Http\Middleware\EnsureTokenBelongsToCurrentTeamMember;
use App\Models\Environment;
use App\Models\InstanceSettings;
use App\Models\Project;
use App\Models\Server;
use App\Models\Service;
use App\Models\ServiceDatabase;
use App\Models\StandaloneDocker;
use App\Models\Team;
use App\Models\User;
@@ -26,9 +30,6 @@ beforeEach(function () {
$this->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'],
]);