From fdb77415a0834f300bdec50b0916b2be68054c35 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 23 Oct 2026 22:21:01 +0200 Subject: [PATCH] fix(api): scope database backup endpoints to the database type Closes #12279 Co-Authored-By: Claude Opus 5.5 --- .../Controllers/Api/DatabasesController.php | 10 +-- .../DatabaseBackupApiTypeScopeTest.php | 86 +++++++++++++++++++ 2 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 tests/Feature/DatabaseBackupApiTypeScopeTest.php diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 5ecab6ed6d..b00d5e7dce 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -444,7 +444,7 @@ class DatabasesController extends Controller $this->authorize('view', $database); $messageSecrets = []; - $backupConfig = ScheduledDatabaseBackup::ownedByCurrentTeamAPI($teamId)->with(['executions', 's3', 's3Storages'])->where('database_id', $database->id)->get() + $backupConfig = ScheduledDatabaseBackup::ownedByCurrentTeamAPI($teamId)->with(['executions', 's3', 's3Storages'])->where('database_id', $database->id)->where('database_type', $database->getMorphClass())->get() ->map(function (ScheduledDatabaseBackup $backup) use ($database, &$messageSecrets): ScheduledDatabaseBackup { $backup->executions->each(function (ScheduledDatabaseBackupExecution $execution) use ($database, &$messageSecrets): void { $this->redactBackupExecutionMessage($execution, $database, $messageSecrets); @@ -1382,7 +1382,7 @@ class DatabasesController extends Controller return $s3Storages; } - $backupConfig = ScheduledDatabaseBackup::ownedByCurrentTeamAPI($teamId)->where('database_id', $database->id) + $backupConfig = ScheduledDatabaseBackup::ownedByCurrentTeamAPI($teamId)->where('database_id', $database->id)->where('database_type', $database->getMorphClass()) ->where('uuid', $request->scheduled_backup_uuid) ->first(); if (! $backupConfig) { @@ -3061,7 +3061,7 @@ class DatabasesController extends Controller $this->authorize('update', $database); // Find the backup configuration by its UUID - $backup = ScheduledDatabaseBackup::ownedByCurrentTeamAPI($teamId)->where('database_id', $database->id) + $backup = ScheduledDatabaseBackup::ownedByCurrentTeamAPI($teamId)->where('database_id', $database->id)->where('database_type', $database->getMorphClass()) ->where('uuid', $request->scheduled_backup_uuid) ->first(); @@ -3197,7 +3197,7 @@ class DatabasesController extends Controller $this->authorize('update', $database); // Find the backup configuration by its UUID - $backup = ScheduledDatabaseBackup::ownedByCurrentTeamAPI($teamId)->where('database_id', $database->id) + $backup = ScheduledDatabaseBackup::ownedByCurrentTeamAPI($teamId)->where('database_id', $database->id)->where('database_type', $database->getMorphClass()) ->where('uuid', $request->scheduled_backup_uuid) ->first(); @@ -3315,7 +3315,7 @@ class DatabasesController extends Controller } // Find the backup configuration by its UUID - $backup = ScheduledDatabaseBackup::ownedByCurrentTeamAPI($teamId)->where('database_id', $database->id) + $backup = ScheduledDatabaseBackup::ownedByCurrentTeamAPI($teamId)->where('database_id', $database->id)->where('database_type', $database->getMorphClass()) ->where('uuid', $request->scheduled_backup_uuid) ->first(); diff --git a/tests/Feature/DatabaseBackupApiTypeScopeTest.php b/tests/Feature/DatabaseBackupApiTypeScopeTest.php new file mode 100644 index 0000000000..6a1b03ec0e --- /dev/null +++ b/tests/Feature/DatabaseBackupApiTypeScopeTest.php @@ -0,0 +1,86 @@ +set('app.maintenance.store', 'array'); + InstanceSettings::forceCreate(['id' => 0, 'is_api_enabled' => true]); + + $this->team = Team::factory()->create(); + $server = Server::factory()->create(['team_id' => $this->team->id]); + $destination = StandaloneDocker::query()->where('server_id', $server->id)->firstOrFail(); + $environment = Environment::factory()->create(['project_id' => Project::factory()->create(['team_id' => $this->team->id])->id]); + $common = [ + 'id' => 7, + 'environment_id' => $environment->id, + 'destination_id' => $destination->id, + 'destination_type' => $destination->getMorphClass(), + ]; + + $this->mysql = StandaloneMysql::forceCreate([...$common, 'name' => 'mysql', 'mysql_root_password' => 'root', 'mysql_password' => 'pw']); + $this->mariadb = StandaloneMariadb::forceCreate([...$common, 'name' => 'mariadb', 'mariadb_root_password' => 'root', 'mariadb_password' => 'pw']); + + $this->mariadbBackup = ScheduledDatabaseBackup::query()->create([ + 'team_id' => $this->team->id, + 'frequency' => '0 0 * * *', + 'database_type' => $this->mariadb->getMorphClass(), + 'database_id' => $this->mariadb->id, + ]); + ScheduledDatabaseBackupExecution::query()->create([ + 'scheduled_database_backup_id' => $this->mariadbBackup->id, + 'status' => 'success', + ]); + + $user = User::factory()->create(); + $this->team->members()->attach($user->id, ['role' => 'owner']); + $plainTextToken = Str::random(40); + $token = $user->tokens()->create([ + 'name' => 'test-token', + 'token' => hash('sha256', $plainTextToken), + 'abilities' => ['root'], + 'team_id' => $this->team->id, + ]); + $this->bearer = $token->getKey().'|'.$plainTextToken; +}); + +it('lists only the backups of the requested database type', function () { + expect($this->mysql->id)->toBe($this->mariadb->id); + + $this->withToken($this->bearer)->getJson("/api/v1/databases/{$this->mysql->uuid}/backups") + ->assertOk() + ->assertExactJson([]); + + $this->withToken($this->bearer)->getJson("/api/v1/databases/{$this->mariadb->uuid}/backups") + ->assertOk() + ->assertJsonCount(1) + ->assertJsonPath('0.uuid', $this->mariadbBackup->uuid); +}); + +it('does not expose a backup of another database type with the same id', function (string $method, Closure $path, array $payload = []) { + $this->withToken($this->bearer)->json($method, $path(), $payload)->assertNotFound(); + + expect($this->mariadbBackup->fresh())->not->toBeNull() + ->and($this->mariadbBackup->fresh()->frequency)->toBe('0 0 * * *') + ->and($this->mariadbBackup->executions()->count())->toBe(1); +})->with([ + 'list executions' => ['GET', fn () => '/api/v1/databases/'.test()->mysql->uuid.'/backups/'.test()->mariadbBackup->uuid.'/executions'], + 'update backup' => ['PATCH', fn () => '/api/v1/databases/'.test()->mysql->uuid.'/backups/'.test()->mariadbBackup->uuid, ['frequency' => '0 1 * * *']], + 'delete backup' => ['DELETE', fn () => '/api/v1/databases/'.test()->mysql->uuid.'/backups/'.test()->mariadbBackup->uuid], + 'delete execution' => ['DELETE', fn () => '/api/v1/databases/'.test()->mysql->uuid.'/backups/'.test()->mariadbBackup->uuid.'/executions/'.test()->mariadbBackup->executions()->first()->uuid], +]);