feat(terminal-access): implement terminal access control for servers and containers, including UI updates and backend logic

This commit is contained in:
Andras Bacsai
2025-05-29 14:09:05 +02:00
parent e9deaca8cd
commit 46b4cfac68
11 changed files with 149 additions and 25 deletions

View File

@@ -49,17 +49,18 @@ class ExecuteContainerCommand extends Component
if (! auth()->user()->isAdmin()) {
abort(403);
}
$this->parameters = get_route_parameters();
$this->containers = collect();
$this->servers = collect();
if (data_get($this->parameters, 'application_uuid')) {
$this->type = 'application';
$this->resource = Application::where('uuid', $this->parameters['application_uuid'])->firstOrFail();
if ($this->resource->destination->server->isFunctional()) {
if ($this->resource->destination->server->isFunctional() && $this->resource->destination->server->isTerminalEnabled()) {
$this->servers = $this->servers->push($this->resource->destination->server);
}
foreach ($this->resource->additional_servers as $server) {
if ($server->isFunctional()) {
if ($server->isFunctional() && $server->isTerminalEnabled()) {
$this->servers = $this->servers->push($server);
}
}
@@ -71,14 +72,14 @@ class ExecuteContainerCommand extends Component
abort(404);
}
$this->resource = $resource;
if ($this->resource->destination->server->isFunctional()) {
if ($this->resource->destination->server->isFunctional() && $this->resource->destination->server->isTerminalEnabled()) {
$this->servers = $this->servers->push($this->resource->destination->server);
}
$this->loadContainers();
} elseif (data_get($this->parameters, 'service_uuid')) {
$this->type = 'service';
$this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail();
if ($this->resource->server->isFunctional()) {
if ($this->resource->server->isFunctional() && $this->resource->server->isTerminalEnabled()) {
$this->servers = $this->servers->push($this->resource->server);
}
$this->loadContainers();

View File

@@ -4,9 +4,12 @@ namespace App\Livewire\Server;
use App\Helpers\SslHelper;
use App\Jobs\RegenerateSslCertJob;
use App\Models\InstanceSettings;
use App\Models\Server;
use App\Models\SslCertificate;
use Carbon\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Attributes\Validate;
use Livewire\Component;
@@ -36,6 +39,9 @@ class Advanced extends Component
#[Validate(['integer', 'min:1'])]
public int $dynamicTimeout = 1;
#[Validate(['boolean'])]
public bool $isTerminalEnabled = false;
public function mount(string $server_uuid)
{
try {
@@ -63,6 +69,37 @@ class Advanced extends Component
$this->showCertificate = ! $this->showCertificate;
}
public function toggleTerminal($password)
{
try {
// Check if user is admin or owner
if (! auth()->user()->isAdmin()) {
throw new \Exception('Only team administrators and owners can modify terminal access.');
}
// Verify password unless two-step confirmation is disabled
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
}
// Toggle the terminal setting
$this->server->settings->is_terminal_enabled = ! $this->server->settings->is_terminal_enabled;
$this->server->settings->save();
// Update the local property
$this->isTerminalEnabled = $this->server->settings->is_terminal_enabled;
$status = $this->isTerminalEnabled ? 'enabled' : 'disabled';
$this->dispatch('success', "Terminal access has been {$status}.");
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function saveCaCertificate()
{
try {
@@ -149,6 +186,7 @@ class Advanced extends Component
$this->dynamicTimeout = $this->server->settings->dynamic_timeout;
$this->serverDiskUsageNotificationThreshold = $this->server->settings->server_disk_usage_notification_threshold;
$this->serverDiskUsageCheckFrequency = $this->server->settings->server_disk_usage_check_frequency;
$this->isTerminalEnabled = $this->server->settings->is_terminal_enabled;
}
}

View File

@@ -21,7 +21,9 @@ class Index extends Component
if (! auth()->user()->isAdmin()) {
abort(403);
}
$this->servers = Server::isReachable()->get();
$this->servers = Server::isReachable()->get()->filter(function ($server) {
return $server->isTerminalEnabled();
});
}
public function loadContainers()

View File

@@ -952,6 +952,11 @@ $schema://$host {
}
}
public function isTerminalEnabled()
{
return $this->settings->is_terminal_enabled ?? false;
}
public function isSwarm()
{
return data_get($this, 'settings.is_swarm_manager') || data_get($this, 'settings.is_swarm_worker');

View File

@@ -28,6 +28,7 @@ use OpenApi\Attributes as OA;
'is_sentinel_enabled' => ['type' => 'boolean'],
'is_swarm_manager' => ['type' => 'boolean'],
'is_swarm_worker' => ['type' => 'boolean'],
'is_terminal_enabled' => ['type' => 'boolean'],
'is_usable' => ['type' => 'boolean'],
'logdrain_axiom_api_key' => ['type' => 'string'],
'logdrain_axiom_dataset_name' => ['type' => 'string'],
@@ -59,6 +60,7 @@ class ServerSetting extends Model
'sentinel_token' => 'encrypted',
'is_reachable' => 'boolean',
'is_usable' => 'boolean',
'is_terminal_enabled' => 'boolean',
];
protected static function booted()

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('server_settings', function (Blueprint $table) {
$table->boolean('is_terminal_enabled')->default(true);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('server_settings', function (Blueprint $table) {
$table->dropColumn('is_terminal_enabled');
});
}
};

View File

@@ -23,11 +23,15 @@
'dispatchEventType' => 'success',
'dispatchEventMessage' => '',
'ignoreWire' => true,
'temporaryDisableTwoStepConfirmation' => false,
])
@php
use App\Models\InstanceSettings;
$disableTwoStepConfirmation = data_get(InstanceSettings::get(), 'disable_two_step_confirmation');
if ($temporaryDisableTwoStepConfirmation) {
$disableTwoStepConfirmation = false;
}
@endphp
<div {{ $ignoreWire ? 'wire:ignore' : '' }} x-data="{
@@ -262,7 +266,7 @@
{{ $shortConfirmationLabel }}
</label>
<input type="text" x-model="userConfirmationText"
class="p-2 mt-1 w-full text-black rounded-sm input">
class="p-2 mt-1 px-3 w-full rounded-sm input">
</div>
@endif
@endif

View File

@@ -44,7 +44,7 @@
</div>
@else
@if (count($containers) === 0)
<div class="pt-4">No containers are running.</div>
<div class="pt-4">No containers are running on this server or terminal access is disabled.</div>
@else
@if (count($containers) === 1)
<form class="w-full pt-4" wire:submit="$dispatchSelf('connectToContainer')"

View File

@@ -14,6 +14,46 @@
<div class="mb-4">Advanced configuration for your server.</div>
</div>
<div class="flex items-center gap-2">
<h3>Terminal Access</h3>
<x-helper
helper="Control whether terminal access is available for this server and its containers.<br/>Only team
administrators and owners can modify this setting." />
@if ($isTerminalEnabled)
<span
class="px-2 py-1 text-xs font-semibold text-green-800 bg-green-100 rounded dark:text-green-100 dark:bg-green-800">
Enabled
</span>
@else
<span
class="px-2 py-1 text-xs font-semibold text-red-800 bg-red-100 rounded dark:text-red-100 dark:bg-red-800">
Disabled
</span>
@endif
</div>
<div class="flex flex-col gap-4">
<div class="flex items-center gap-4 pt-4">
@if (auth()->user()->isAdmin())
<div wire:key="terminal-access-change-{{ $isTerminalEnabled }}" class="pb-4">
<x-modal-confirmation title="Confirm Terminal Access Change?"
buttonTitle="{{ $isTerminalEnabled ? 'Disable Terminal' : 'Enable Terminal' }}"
submitAction="toggleTerminal" :actions="[
$isTerminalEnabled
? 'This will disable terminal access for this server and all its containers.'
: 'This will enable terminal access for this server and all its containers.',
$isTerminalEnabled
? 'Users will no longer be able to access terminal views from the UI.'
: 'Users will be able to access terminal views from the UI.',
'This change will take effect immediately.',
]" confirmationText="{{ $server->name }}"
shortConfirmationLabel="Server Name"
step3ButtonText="{{ $isTerminalEnabled ? 'Disable Terminal' : 'Enable Terminal' }}">
</x-modal-confirmation>
</div>
@endif
</div>
</div>
<h3>Disk Usage</h3>
<div class="flex flex-col gap-6">
<div class="flex flex-col">

View File

@@ -14,6 +14,7 @@
<x-loading text="Loading servers and containers..." />
</div>
@else
@if ($servers->count() > 0)
<form class="flex flex-col gap-2 justify-center xl:items-end xl:flex-row"
wire:submit="$dispatchSelf('connectToContainer')">
<x-forms.select id="server" required wire:model.live="selected_uuid">
@@ -33,6 +34,9 @@
</x-forms.select>
<x-forms.button type="submit">Connect</x-forms.button>
</form>
@else
<div>No servers with terminal access found.</div>
@endif
@endif
<livewire:project.shared.terminal />
</div>

View File

@@ -153,7 +153,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::post('/terminal/auth/ips', function () {
if (auth()->check()) {
$team = auth()->user()->currentTeam();
$ipAddresses = $team->servers()->pluck('ip')->toArray();
$ipAddresses = $team->servers->where('settings.is_terminal_enabled', true)->pluck('ip')->toArray();
return response()->json(['ipAddresses' => $ipAddresses], 200);
}