Files
coolify/app/Livewire/Project/Database/Clickhouse/General.php
T
Andras Bacsai 88efc0716e fix: enforce resource team isolation and preserve ClickHouse data
Scope resource permissions and related records to the owning team across
team switches, cloning, deployments, and resource operations.

Merge ClickHouse volume mounts, preserve anonymous data volumes on stop,
and require adopting current data before starting or restarting.

Fix file storage deletion checks and add regression coverage.
2026-10-05 14:23:04 +02:00

261 lines
9.5 KiB
PHP

<?php
namespace App\Livewire\Project\Database\Clickhouse;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Models\Server;
use App\Models\StandaloneClickhouse;
use App\Support\ValidationPatterns;
use App\Traits\ListensToTeamChannel;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Locked;
use Livewire\Component;
class General extends Component
{
use AuthorizesRequests;
use ListensToTeamChannel;
public ?Server $server = null;
public StandaloneClickhouse $database;
public string $name;
public ?string $description = null;
public string $clickhouseAdminUser;
public string $clickhouseAdminPassword;
public string $image;
public ?string $portsMappings = null;
public ?bool $isPublic = null;
public mixed $publicPort = null;
public mixed $publicPortTimeout = 3600;
public ?string $customDockerRunOptions = null;
public bool $isLogDrainEnabled = false;
public bool $isPasswordHiddenForMember = false;
/** Unnamed Docker volume that holds the current data; shown as a warning. */
#[Locked]
public ?string $anonymousDataVolume = null;
public function getListeners(): array
{
return $this->teamChannelListeners([
'DatabaseProxyStopped' => 'databaseProxyStopped',
]);
}
public function mount()
{
try {
$this->authorize('view', $this->database);
$this->syncData();
$this->server = data_get($this->database, 'destination.server');
if (! $this->server) {
$this->dispatch('error', 'Database destination server is not configured.');
return;
}
$this->anonymousDataVolume = $this->database->anonymousDataVolume();
} catch (\Throwable $e) {
return handleError($e, $this);
}
$this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
}
protected function rules(): array
{
return [
'name' => ValidationPatterns::nameRules(),
'description' => ValidationPatterns::descriptionRules(),
'clickhouseAdminUser' => ValidationPatterns::databaseIdentifierRules(
enforcePattern: $this->clickhouseAdminUser !== $this->database->clickhouse_admin_user,
),
'clickhouseAdminPassword' => ValidationPatterns::databasePasswordRules(
enforcePattern: $this->clickhouseAdminPassword !== $this->database->clickhouse_admin_password,
),
'image' => 'required|string',
'portsMappings' => ValidationPatterns::portMappingRules(),
'isPublic' => 'nullable|boolean',
'publicPort' => 'nullable|integer|min:1|max:65535',
'publicPortTimeout' => 'nullable|integer|min:1',
'customDockerRunOptions' => 'nullable|string',
'isLogDrainEnabled' => 'nullable|boolean',
];
}
protected function messages(): array
{
return array_merge(
ValidationPatterns::combinedMessages(),
ValidationPatterns::portMappingMessages(),
[
...ValidationPatterns::databaseIdentifierMessages('clickhouseAdminUser', 'Admin User'),
...ValidationPatterns::databasePasswordMessages('clickhouseAdminPassword', 'Admin Password'),
'image.required' => 'The Docker Image field is required.',
'image.string' => 'The Docker Image must be a string.',
'publicPort.integer' => 'The Public Port must be an integer.',
'publicPort.min' => 'The Public Port must be at least 1.',
'publicPort.max' => 'The Public Port must not exceed 65535.',
'publicPortTimeout.integer' => 'The Public Port Timeout must be an integer.',
'publicPortTimeout.min' => 'The Public Port Timeout must be at least 1.',
]
);
}
private function syncData(bool $toModel = false): void
{
if ($toModel) {
$this->validate();
$this->database->name = $this->name;
$this->database->description = $this->description;
$this->database->clickhouse_admin_user = $this->clickhouseAdminUser;
$this->database->clickhouse_admin_password = $this->clickhouseAdminPassword;
$this->database->image = $this->image;
$this->database->ports_mappings = $this->portsMappings;
$this->database->is_public = $this->isPublic;
$this->database->public_port = $this->publicPort ?: null;
$this->database->public_port_timeout = $this->publicPortTimeout ?: null;
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
$this->database->save();
} else {
$this->name = $this->database->name;
$this->description = $this->database->description;
$this->clickhouseAdminUser = $this->database->clickhouse_admin_user;
$canSeeCredentials = auth()->user()?->can('update', $this->database) ?? false;
$this->clickhouseAdminPassword = $canSeeCredentials ? $this->database->clickhouse_admin_password : '';
$this->image = $this->database->image;
$this->portsMappings = $this->database->ports_mappings;
$this->isPublic = $this->database->is_public;
$this->publicPort = $this->database->public_port;
$this->publicPortTimeout = $this->database->public_port_timeout;
$this->customDockerRunOptions = $this->database->custom_docker_run_options;
$this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
}
}
public function instantSaveAdvanced()
{
try {
$this->authorize('update', $this->database);
if (! $this->server->isLogDrainEnabled()) {
$this->isLogDrainEnabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
$this->syncData(true);
$this->dispatch('success', 'Database updated.');
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function instantSave()
{
try {
$this->authorize('update', $this->database);
if ($this->isPublic && ! $this->publicPort) {
$this->dispatch('error', 'Public port is required.');
$this->isPublic = false;
return;
}
if ($this->isPublic && ! str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->isPublic = false;
return;
}
$this->syncData(true);
if ($this->isPublic) {
StartDatabaseProxy::run($this->database);
$this->dispatch('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);
$this->dispatch('success', 'Database is no longer publicly accessible.');
}
$this->dispatch('databaseUpdated');
} catch (\Throwable $e) {
$this->authorize('update', $this->database);
$this->isPublic = ! $this->isPublic;
$this->syncData(true);
return handleError($e, $this);
}
}
public function keepCurrentDataVolume(): void
{
try {
$this->authorize('update', $this->database);
$volumeName = $this->database->anonymousDataVolume();
if ($volumeName === null) {
$this->anonymousDataVolume = null;
$this->dispatch('info', 'This database already uses its data volume.');
return;
}
$this->database->keepAnonymousDataVolume($volumeName);
$this->anonymousDataVolume = null;
$this->dispatch('success', 'The current data volume is now the data volume of this database. You can restart it safely.');
} catch (\Throwable $e) {
handleError($e, $this);
}
}
public function databaseProxyStopped(): void
{
$this->database->refresh();
$this->isPublic = $this->database->is_public;
$this->publicPort = $this->database->public_port;
$this->publicPortTimeout = $this->database->public_port_timeout;
$this->dispatch('databaseUpdated');
}
public function submit()
{
try {
$this->authorize('update', $this->database);
if ($this->portsMappings) {
$this->portsMappings = str($this->portsMappings)->replace(' ', '')->trim()->toString();
}
if (str($this->publicPort)->isEmpty()) {
$this->publicPort = null;
}
$this->syncData(true);
$this->dispatch('success', 'Database updated.');
$this->dispatch('databaseUpdated');
} catch (Exception $e) {
return handleError($e, $this);
} finally {
if (is_null($this->database->config_hash)) {
$this->database->isConfigurationChanged(true);
} else {
$this->dispatch('configurationChanged');
}
}
}
}