CluPilotCloud/app/Console/Commands/ConsoleAccess.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;
}
}