CluPilotCloud/app/Services/Terminal/ServerTicket.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,
]);
}
}