91 lines
3.9 KiB
PHP
91 lines
3.9 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Terminal;
|
|
|
|
use App\Models\Operator;
|
|
use App\Services\Secrets\SecretVault;
|
|
use Illuminate\Support\Facades\Log;
|
|
use RuntimeException;
|
|
|
|
/**
|
|
* Die Eintrittskarte für eine Root-Sitzung auf dem CluPilot-Server selbst.
|
|
*
|
|
* Dieselbe Form wie {@see TerminalTicket}, dieselbe Brücke, dieselben dreißig
|
|
* Sekunden — nur eine andere Herkunft der vier Felder: Adresse und
|
|
* Fingerabdruck aus der Einrichtungsdatei des Wirts, Benutzer `root`, und der
|
|
* Schlüssel aus einem EIGENEN Tresor-Eintrag.
|
|
*
|
|
* `host_uuid` trägt den festen Wert `server`. Die Brücke schreibt dieses Feld
|
|
* in ihre Log-Zeile; ein leeres Feld hätte sie unlesbar gemacht, ein neues Feld
|
|
* hätte sie geändert. So bleibt sie unverändert und das Log bleibt lesbar.
|
|
*
|
|
* WAS HIER NICHT GEPRÜFT WIRD: von welcher Adresse aus jemand fragt. Das ist
|
|
* eine Eigenschaft der Anfrage, nicht der Maschine, und steht deshalb in der
|
|
* Seite (App\Livewire\Admin\ServerTerminal) — vor dem einzigen Aufruf von
|
|
* issue(), den es gibt.
|
|
*/
|
|
final class ServerTicket
|
|
{
|
|
/**
|
|
* Was gerade im Weg steht — oder null, wenn nichts im Weg steht.
|
|
*
|
|
* Es gibt diese Methode aus demselben Grund wie beim Host-Terminal: die
|
|
* Seite will die Antwort VORHER wissen, um sie in ihrer eigenen Sprache
|
|
* hinzuschreiben, statt sie als 500er weiterzureichen. Zurück kommt ein
|
|
* Merkwort, kein Satz.
|
|
*/
|
|
public static function blocker(): ?string
|
|
{
|
|
if (! ServerIdentity::exists()) {
|
|
return 'no_key';
|
|
}
|
|
|
|
// Ohne die Einrichtung fehlen Adresse UND Fingerabdruck. Beide kommen
|
|
// aus derselben Datei und werden von derselben Zeile geschrieben, also
|
|
// gibt es auch nur ein Merkwort dafür — und genau eine Anleitung.
|
|
if (ServerTerminalSetup::read() === null) {
|
|
return 'not_installed';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public static function issue(Operator $for): string
|
|
{
|
|
match (self::blocker()) {
|
|
'no_key' => throw new RuntimeException('Für den Server ist keine SSH-Kennung erzeugt — ohne sie kann keine Terminalsitzung entstehen.'),
|
|
'not_installed' => throw new RuntimeException('Der Server ist für das Terminal noch nicht eingerichtet — es fehlt storage/app/deploy/server-terminal.json.'),
|
|
default => null,
|
|
};
|
|
|
|
// blocker() hat die Datei eben gelesen; dass sie zwischen den beiden
|
|
// Zeilen verschwindet, wäre ein Wettlauf mit dem Einrichtungsskript.
|
|
// Die Prüfung steht trotzdem da: die Alternative wäre ein Zugriff auf
|
|
// null — also eine Fehlerseite in einem Terminalfenster.
|
|
$setup = ServerTerminalSetup::read()
|
|
?? throw new RuntimeException('Die Einrichtung des Server-Terminals ist zwischen Prüfung und Ausstellung verschwunden.');
|
|
|
|
// Wer eine Root-Konsole in die Steuerungsebene aufmacht, steht mit
|
|
// Namen im Log. Die Brücke kennt nur die Nummer des Betreibers und
|
|
// nicht die Adresse, von der aus gefragt wurde — beides weiß allein
|
|
// diese Seite der Anwendung, und ohne diese Zeile wäre es nirgends
|
|
// festgehalten. Der INHALT der Sitzung wird bewusst nicht
|
|
// mitgeschrieben: er enthält zwangsläufig Passwörter und Schlüssel im
|
|
// Klartext, und ein Mitschnitt davon wäre ein zweites Geheimnislager.
|
|
Log::warning('Root-Konsole zum CluPilot-Server angefordert.', [
|
|
'operator' => $for->name,
|
|
'operator_id' => $for->id,
|
|
'from' => request()->ip(),
|
|
]);
|
|
|
|
return TicketStore::issue([
|
|
'operator_id' => $for->id,
|
|
'host_uuid' => 'server',
|
|
'ip' => $setup->address,
|
|
'user' => 'root',
|
|
'private_key' => (string) app(SecretVault::class)->get(ServerIdentity::VAULT_KEY),
|
|
'fingerprint' => $setup->fingerprint,
|
|
]);
|
|
}
|
|
}
|