Add shared environment variable key validation and normalization for Livewire forms and models, allowing Docker-compatible keys while rejecting invalid entries such as keys containing equals signs. Also quote Railpack build environment and secret arguments safely.
379 lines
13 KiB
PHP
379 lines
13 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire\Project\Shared\EnvironmentVariable;
|
|
|
|
use App\Models\Application;
|
|
use App\Models\Environment;
|
|
use App\Models\EnvironmentVariable as ModelsEnvironmentVariable;
|
|
use App\Models\Project;
|
|
use App\Models\Server;
|
|
use App\Models\Service;
|
|
use App\Models\SharedEnvironmentVariable;
|
|
use App\Support\ValidationPatterns;
|
|
use App\Traits\EnvironmentVariableAnalyzer;
|
|
use App\Traits\EnvironmentVariableProtection;
|
|
use Illuminate\Auth\Access\AuthorizationException;
|
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
|
use Livewire\Attributes\Computed;
|
|
use Livewire\Component;
|
|
|
|
class Show extends Component
|
|
{
|
|
use AuthorizesRequests, EnvironmentVariableAnalyzer, EnvironmentVariableProtection;
|
|
|
|
public $parameters;
|
|
|
|
public ModelsEnvironmentVariable|SharedEnvironmentVariable $env;
|
|
|
|
public bool $isDisabled = false;
|
|
|
|
public bool $isLocked = false;
|
|
|
|
public bool $isMagicVariable = false;
|
|
|
|
public bool $isSharedVariable = false;
|
|
|
|
public string $type;
|
|
|
|
public string $key;
|
|
|
|
public ?string $value = null;
|
|
|
|
public ?string $real_value = null;
|
|
|
|
public ?string $comment = null;
|
|
|
|
public bool $is_shared = false;
|
|
|
|
public bool $is_multiline = false;
|
|
|
|
public bool $is_literal = false;
|
|
|
|
public bool $is_shown_once = false;
|
|
|
|
public bool $is_runtime = true;
|
|
|
|
public bool $is_buildtime = true;
|
|
|
|
public bool $is_required = false;
|
|
|
|
public bool $is_really_required = false;
|
|
|
|
public bool $is_redis_credential = false;
|
|
|
|
public array $problematicVariables = [];
|
|
|
|
protected $listeners = [
|
|
'refreshEnvs' => 'refresh',
|
|
'refresh',
|
|
'compose_loaded' => '$refresh',
|
|
];
|
|
|
|
protected function rules(): array
|
|
{
|
|
return [
|
|
'key' => ValidationPatterns::environmentVariableKeyRules(),
|
|
'value' => 'nullable',
|
|
'comment' => 'nullable|string|max:256',
|
|
'is_multiline' => 'required|boolean',
|
|
'is_literal' => 'required|boolean',
|
|
'is_shown_once' => 'required|boolean',
|
|
'is_runtime' => 'required|boolean',
|
|
'is_buildtime' => 'required|boolean',
|
|
'real_value' => 'nullable',
|
|
'is_required' => 'required|boolean',
|
|
];
|
|
}
|
|
|
|
protected function messages(): array
|
|
{
|
|
return ValidationPatterns::environmentVariableKeyMessages('key');
|
|
}
|
|
|
|
public function mount()
|
|
{
|
|
$this->syncData();
|
|
if ($this->env->getMorphClass() === SharedEnvironmentVariable::class) {
|
|
$this->isSharedVariable = true;
|
|
}
|
|
$this->parameters = get_route_parameters();
|
|
$this->checkEnvs();
|
|
if ($this->type === 'standalone-redis' && ($this->env->key === 'REDIS_PASSWORD' || $this->env->key === 'REDIS_USERNAME')) {
|
|
$this->is_redis_credential = true;
|
|
}
|
|
$this->problematicVariables = self::getProblematicVariablesForFrontend();
|
|
}
|
|
|
|
public function getResourceProperty()
|
|
{
|
|
return $this->env->resourceable ?? $this->env;
|
|
}
|
|
|
|
public function refresh()
|
|
{
|
|
if (! $this->env->exists || ! $this->env->fresh()) {
|
|
return;
|
|
}
|
|
$this->syncData();
|
|
$this->checkEnvs();
|
|
}
|
|
|
|
public function syncData(bool $toModel = false)
|
|
{
|
|
if ($toModel) {
|
|
$this->key = ValidationPatterns::normalizeEnvironmentVariableKey($this->key);
|
|
|
|
if ($this->isSharedVariable) {
|
|
$this->validate([
|
|
'key' => ValidationPatterns::environmentVariableKeyRules(),
|
|
'value' => 'nullable',
|
|
'comment' => 'nullable|string|max:256',
|
|
'is_multiline' => 'required|boolean',
|
|
'is_literal' => 'required|boolean',
|
|
'is_shown_once' => 'required|boolean',
|
|
'real_value' => 'nullable',
|
|
]);
|
|
} else {
|
|
$this->validate();
|
|
$this->env->is_required = $this->is_required;
|
|
$this->env->is_runtime = $this->is_runtime;
|
|
$this->env->is_buildtime = $this->is_buildtime;
|
|
$this->env->is_shared = $this->is_shared;
|
|
}
|
|
$this->env->key = $this->key;
|
|
$this->env->value = $this->value;
|
|
$this->env->comment = $this->comment;
|
|
$this->env->is_multiline = $this->is_multiline;
|
|
$this->env->is_literal = $this->is_literal;
|
|
$this->env->is_shown_once = $this->is_shown_once;
|
|
$this->env->save();
|
|
} else {
|
|
$this->key = $this->env->key;
|
|
$this->value = $this->env->value;
|
|
$this->comment = $this->env->comment;
|
|
$this->is_multiline = $this->env->is_multiline;
|
|
$this->is_literal = $this->env->is_literal;
|
|
$this->is_shown_once = $this->env->is_shown_once;
|
|
$this->is_runtime = $this->env->is_runtime ?? true;
|
|
$this->is_buildtime = $this->env->is_buildtime ?? true;
|
|
$this->is_required = $this->env->is_required ?? false;
|
|
$this->is_really_required = $this->env->is_really_required ?? false;
|
|
$this->is_shared = $this->env->is_shared ?? false;
|
|
$this->real_value = $this->env->real_value;
|
|
}
|
|
}
|
|
|
|
public function checkEnvs()
|
|
{
|
|
$this->isDisabled = false;
|
|
$this->isMagicVariable = false;
|
|
|
|
if (str($this->env->key)->startsWith('SERVICE_FQDN') || str($this->env->key)->startsWith('SERVICE_URL') || str($this->env->key)->startsWith('SERVICE_NAME')) {
|
|
$this->isDisabled = true;
|
|
$this->isMagicVariable = true;
|
|
}
|
|
|
|
if ($this->env->is_shown_once) {
|
|
$this->isLocked = true;
|
|
}
|
|
}
|
|
|
|
public function serialize()
|
|
{
|
|
data_forget($this->env, 'real_value');
|
|
}
|
|
|
|
public function lock()
|
|
{
|
|
$this->authorize('update', $this->env);
|
|
|
|
$this->env->is_shown_once = true;
|
|
if ($this->isSharedVariable) {
|
|
unset($this->env->is_required);
|
|
}
|
|
$this->serialize();
|
|
$this->env->save();
|
|
$this->checkEnvs();
|
|
$this->dispatch('refreshEnvs');
|
|
}
|
|
|
|
public function instantSave()
|
|
{
|
|
$this->submit();
|
|
}
|
|
|
|
public function submit()
|
|
{
|
|
try {
|
|
$this->authorize('update', $this->env);
|
|
|
|
if (! $this->isSharedVariable && $this->is_required && str($this->value)->isEmpty()) {
|
|
$oldValue = $this->env->getOriginal('value');
|
|
$this->value = $oldValue;
|
|
$this->dispatch('error', 'Required environment variables cannot be empty.');
|
|
|
|
return;
|
|
}
|
|
|
|
$this->serialize();
|
|
$this->syncData(true);
|
|
$this->syncData(false);
|
|
$this->dispatch('success', 'Environment variable updated.');
|
|
$this->dispatch('envsUpdated');
|
|
$this->dispatch('configurationChanged');
|
|
} catch (\Exception $e) {
|
|
return handleError($e);
|
|
}
|
|
}
|
|
|
|
#[Computed]
|
|
public function availableSharedVariables(): array
|
|
{
|
|
$team = currentTeam();
|
|
$result = [
|
|
'team' => [],
|
|
'project' => [],
|
|
'environment' => [],
|
|
'server' => [],
|
|
];
|
|
|
|
// Early return if no team
|
|
if (! $team) {
|
|
return $result;
|
|
}
|
|
|
|
// Check if user can view team variables
|
|
try {
|
|
$this->authorize('view', $team);
|
|
$result['team'] = $team->environment_variables()
|
|
->pluck('key')
|
|
->toArray();
|
|
} catch (AuthorizationException $e) {
|
|
// User not authorized to view team variables
|
|
}
|
|
|
|
// Get project variables if we have a project_uuid in route
|
|
$projectUuid = data_get($this->parameters, 'project_uuid');
|
|
if ($projectUuid) {
|
|
$project = Project::where('team_id', $team->id)
|
|
->where('uuid', $projectUuid)
|
|
->first();
|
|
|
|
if ($project) {
|
|
try {
|
|
$this->authorize('view', $project);
|
|
$result['project'] = $project->environment_variables()
|
|
->pluck('key')
|
|
->toArray();
|
|
|
|
// Get environment variables if we have an environment_uuid in route
|
|
$environmentUuid = data_get($this->parameters, 'environment_uuid');
|
|
if ($environmentUuid) {
|
|
$environment = $project->environments()
|
|
->where('uuid', $environmentUuid)
|
|
->first();
|
|
|
|
if ($environment) {
|
|
try {
|
|
$this->authorize('view', $environment);
|
|
$result['environment'] = $environment->environment_variables()
|
|
->pluck('key')
|
|
->toArray();
|
|
} catch (AuthorizationException $e) {
|
|
// User not authorized to view environment variables
|
|
}
|
|
}
|
|
}
|
|
} catch (AuthorizationException $e) {
|
|
// User not authorized to view project variables
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get server variables
|
|
$serverUuid = data_get($this->parameters, 'server_uuid');
|
|
if ($serverUuid) {
|
|
// If we have a specific server_uuid, show variables for that server
|
|
$server = Server::where('team_id', $team->id)
|
|
->where('uuid', $serverUuid)
|
|
->first();
|
|
|
|
if ($server) {
|
|
try {
|
|
$this->authorize('view', $server);
|
|
$result['server'] = $server->environment_variables()
|
|
->pluck('key')
|
|
->toArray();
|
|
} catch (AuthorizationException $e) {
|
|
// User not authorized to view server variables
|
|
}
|
|
}
|
|
} else {
|
|
// For application environment variables, try to use the application's destination server
|
|
$applicationUuid = data_get($this->parameters, 'application_uuid');
|
|
if ($applicationUuid) {
|
|
$application = Application::whereRelation('environment.project.team', 'id', $team->id)
|
|
->where('uuid', $applicationUuid)
|
|
->with('destination.server')
|
|
->first();
|
|
|
|
if ($application && $application->destination && $application->destination->server) {
|
|
try {
|
|
$this->authorize('view', $application->destination->server);
|
|
$result['server'] = $application->destination->server->environment_variables()
|
|
->pluck('key')
|
|
->toArray();
|
|
} catch (AuthorizationException $e) {
|
|
// User not authorized to view server variables
|
|
}
|
|
}
|
|
} else {
|
|
// For service environment variables, try to use the service's server
|
|
$serviceUuid = data_get($this->parameters, 'service_uuid');
|
|
if ($serviceUuid) {
|
|
$service = Service::whereRelation('environment.project.team', 'id', $team->id)
|
|
->where('uuid', $serviceUuid)
|
|
->with('server')
|
|
->first();
|
|
|
|
if ($service && $service->server) {
|
|
try {
|
|
$this->authorize('view', $service->server);
|
|
$result['server'] = $service->server->environment_variables()
|
|
->pluck('key')
|
|
->toArray();
|
|
} catch (AuthorizationException $e) {
|
|
// User not authorized to view server variables
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
public function delete()
|
|
{
|
|
try {
|
|
$this->authorize('delete', $this->env);
|
|
|
|
// Check if the variable is used in Docker Compose
|
|
if ($this->type === 'service' || $this->type === 'application' && $this->env->resourceable?->docker_compose) {
|
|
[$isUsed, $reason] = $this->isEnvironmentVariableUsedInDockerCompose($this->env->key, $this->env->resourceable?->docker_compose);
|
|
|
|
if ($isUsed) {
|
|
$this->dispatch('error', "Cannot delete environment variable '{$this->env->key}' <br><br>Please remove it from the Docker Compose file first.");
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
$this->env->delete();
|
|
$this->dispatch('environmentVariableDeleted');
|
|
$this->dispatch('success', 'Environment variable deleted successfully.');
|
|
} catch (\Exception $e) {
|
|
return handleError($e);
|
|
}
|
|
}
|
|
}
|