104 lines
3.4 KiB
PHP
104 lines
3.4 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Http\Middleware\RestrictConsoleNetwork;
|
|
use App\Support\Settings;
|
|
use Illuminate\Console\Command;
|
|
|
|
/**
|
|
* The way back in when the console has locked its owner out.
|
|
*
|
|
* The allowlist is managed in the console — which is fine until the address you
|
|
* manage it from changes, and then the page that would fix the problem is the
|
|
* page the problem blocks. Every gate needs a door that does not depend on
|
|
* itself, and on a server that door is a shell.
|
|
*/
|
|
class ConsoleAccess extends Command
|
|
{
|
|
protected $signature = 'clupilot:console-access
|
|
{action=show : show|allow|deny|open|close}
|
|
{value? : an address or CIDR, for allow and deny}';
|
|
|
|
protected $description = 'Show or change who may reach the operator console';
|
|
|
|
public function handle(): int
|
|
{
|
|
$action = (string) $this->argument('action');
|
|
$value = trim((string) $this->argument('value'));
|
|
|
|
return match ($action) {
|
|
'show' => $this->show(),
|
|
'allow' => $this->allow($value),
|
|
'deny' => $this->deny($value),
|
|
'open' => $this->setRestricted(false),
|
|
'close' => $this->setRestricted(true),
|
|
default => $this->refuse("Unknown action: {$action}"),
|
|
};
|
|
}
|
|
|
|
private function show(): int
|
|
{
|
|
$this->line(' restricted : '.(RestrictConsoleNetwork::isRestricted() ? 'yes' : 'no — anyone reaching the hostname gets in'));
|
|
$this->line(' always : '.implode(', ', (array) config('admin_access.trusted_ranges', [])).' (VPN, not removable)');
|
|
|
|
$own = (array) Settings::get('console.allowed_ips', []);
|
|
$this->line(' additional : '.($own === [] ? '(none)' : implode(', ', $own)));
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
private function allow(string $value): int
|
|
{
|
|
if ($value === '') {
|
|
return $this->refuse('Give an address or range: clupilot:console-access allow 203.0.113.7');
|
|
}
|
|
|
|
// An entry that matches nothing would be stored, reported as success,
|
|
// and leave whoever is recovering still locked out.
|
|
if (! RestrictConsoleNetwork::isNetwork($value)) {
|
|
return $this->refuse("Not an address or a range: {$value}");
|
|
}
|
|
|
|
$list = (array) Settings::get('console.allowed_ips', []);
|
|
|
|
if (! in_array($value, $list, true)) {
|
|
$list[] = $value;
|
|
Settings::set('console.allowed_ips', array_values($list));
|
|
}
|
|
|
|
$this->info("{$value} may now reach the console.");
|
|
|
|
return $this->show();
|
|
}
|
|
|
|
private function deny(string $value): int
|
|
{
|
|
Settings::set('console.allowed_ips', array_values(array_filter(
|
|
(array) Settings::get('console.allowed_ips', []),
|
|
fn ($entry) => $entry !== $value,
|
|
)));
|
|
|
|
$this->info("{$value} removed.");
|
|
|
|
return $this->show();
|
|
}
|
|
|
|
private function setRestricted(bool $on): int
|
|
{
|
|
// No lock-out check here on purpose: this command IS the recovery path,
|
|
// and it is only reachable by someone who already has the server.
|
|
Settings::set('console.network_restricted', $on);
|
|
$this->info($on ? 'Console restricted to the VPN and the listed addresses.' : 'Restriction lifted.');
|
|
|
|
return $this->show();
|
|
}
|
|
|
|
private function refuse(string $message): int
|
|
{
|
|
$this->error($message);
|
|
|
|
return self::FAILURE;
|
|
}
|
|
}
|