mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
Require deploy permission for API changes that trigger server side effects, including resource deletion, storage access, provider operations, and database imports. Also escape Docker cleanup filters and sanitize log drain server names.
89 lines
4.4 KiB
PHP
89 lines
4.4 KiB
PHP
<?php
|
|
|
|
use App\Actions\Database\StartDatabaseImport;
|
|
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\StandalonePostgresql;
|
|
use App\Models\Team;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Str;
|
|
use Spatie\Activitylog\Models\Activity;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
Server::flushIdentityMap();
|
|
InstanceSettings::forceCreate(['id' => 0, 'is_api_enabled' => true]);
|
|
$this->team = Team::factory()->create();
|
|
$this->user = User::factory()->create();
|
|
$this->team->members()->attach($this->user, ['role' => 'owner']);
|
|
session(['currentTeam' => $this->team]);
|
|
|
|
$server = Server::factory()->create(['team_id' => $this->team->id]);
|
|
$destination = StandaloneDocker::firstOrCreate(['server_id' => $server->id, 'network' => 'coolify'], ['uuid' => (string) Str::uuid(), 'name' => 'docker']);
|
|
$project = Project::factory()->create(['team_id' => $this->team->id]);
|
|
$environment = Environment::factory()->create(['project_id' => $project->id]);
|
|
|
|
$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' => $environment->id, 'destination_id' => $destination->id, 'destination_type' => $destination->getMorphClass()]);
|
|
$service = Service::factory()->create(['environment_id' => $environment->id, 'server_id' => $server->id, 'destination_id' => $destination->id, 'destination_type' => $destination->getMorphClass(), 'docker_compose_raw' => "services:\n postgres:\n image: postgres:17\n"]);
|
|
$serviceDatabase = ServiceDatabase::create(['uuid' => (string) Str::uuid(), 'name' => 'postgres', 'service_id' => $service->id, 'image' => 'postgres:17']);
|
|
|
|
$this->importPaths = [
|
|
'standalone' => "/api/v1/databases/{$database->uuid}/imports",
|
|
'service' => "/api/v1/services/{$service->uuid}/databases/{$serviceDatabase->uuid}/imports",
|
|
];
|
|
});
|
|
|
|
function serverSourceImportToken(User $user, Team $team, array $abilities): string
|
|
{
|
|
$token = $user->createToken('server-source-import', $abilities);
|
|
$token->accessToken->forceFill(['team_id' => $team->id])->save();
|
|
|
|
return $token->plainTextToken;
|
|
}
|
|
|
|
function expectServerSourceImportStarts(): void
|
|
{
|
|
$activity = Activity::create(['log_name' => 'default', 'description' => 'queued', 'properties' => ['status' => 'queued']]);
|
|
$action = Mockery::mock(StartDatabaseImport::class);
|
|
$action->shouldReceive('handle')->once()->andReturn($activity);
|
|
app()->instance(StartDatabaseImport::class, $action);
|
|
}
|
|
|
|
dataset('server source import variants', ['standalone', 'service']);
|
|
dataset('import sources', [
|
|
'upload' => [fn () => ['source' => 'upload', 'upload_id' => (string) Str::uuid()]],
|
|
's3' => [fn () => ['source' => 's3', 's3_storage_uuid' => (string) Str::uuid(), 'path' => '/backups/backup.sql']],
|
|
'server' => [fn () => ['source' => 'server', 'path' => '/tmp/backup.sql']],
|
|
]);
|
|
|
|
test('a write token without deploy cannot start an import from any source', function (string $variant, array $payload) {
|
|
$action = Mockery::mock(StartDatabaseImport::class);
|
|
$action->shouldNotReceive('handle');
|
|
app()->instance(StartDatabaseImport::class, $action);
|
|
|
|
$this->withToken(serverSourceImportToken($this->user, $this->team, ['read', 'write']))
|
|
->postJson($this->importPaths[$variant], $payload)
|
|
->assertForbidden()
|
|
->assertJsonPath('message', 'Missing required permissions: deploy. An import runs the dump in the database container, so it needs a token with the deploy permission.');
|
|
|
|
expect(Activity::query()->count())->toBe(0);
|
|
})->with('server source import variants')->with('import sources');
|
|
|
|
test('a write token with deploy or a root token can start an import from any source', function (string $variant, array $payload, array $abilities) {
|
|
expectServerSourceImportStarts();
|
|
|
|
$this->withToken(serverSourceImportToken($this->user, $this->team, $abilities))
|
|
->postJson($this->importPaths[$variant], $payload)
|
|
->assertAccepted();
|
|
})->with('server source import variants')->with('import sources')->with([
|
|
'write and deploy' => [['read', 'write', 'deploy']],
|
|
'root' => [['root']],
|
|
]);
|