101 lines
4.3 KiB
PHP
101 lines
4.3 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Terminal;
|
|
|
|
use App\Models\Host;
|
|
use App\Models\Operator;
|
|
use App\Services\Secrets\SecretVault;
|
|
use RuntimeException;
|
|
|
|
/**
|
|
* Die Eintrittskarte für eine Terminalsitzung AUF EINEM HOST — und die einzige
|
|
* Stelle, die weiß, was für einen Host darin steht.
|
|
*
|
|
* Wo sie liegt, wie lange und in welcher Form, steht seit dem zweiten
|
|
* Ausstellungsweg in {@see TicketStore}. Hier bleibt allein die Frage, die nur
|
|
* ein Host stellt: welche Tunneladresse, welcher Fingerabdruck, und wann es
|
|
* beides noch gar nicht gibt.
|
|
*/
|
|
final class TerminalTicket
|
|
{
|
|
/**
|
|
* Steht weiter hier, weil Aufrufer und Tests danach fragen — der Wert
|
|
* selbst gehört dem Speicher, nicht diesem Weg.
|
|
*/
|
|
public const TTL_SECONDS = TicketStore::TTL_SECONDS;
|
|
|
|
/**
|
|
* Was diesem Host gerade im Weg steht — oder null, wenn nichts im Weg steht.
|
|
*
|
|
* Es gibt diese Methode, weil dieselbe Frage an zwei Stellen gestellt wird
|
|
* und nur eine davon eine Ausnahme gebrauchen kann. `issue()` unten muss
|
|
* scheitern: es ist die letzte Instanz vor dem Ticket. Die Seite dagegen
|
|
* will die Antwort VORHER wissen, um sie in ihrer eigenen Sprache
|
|
* hinzuschreiben, statt sie als 500er an den Betreiber weiterzureichen.
|
|
*
|
|
* Zurück kommt ein Merkwort, kein Satz: die Formulierung gehört in die
|
|
* Sprachdateien, nicht in eine Ausnahme. `null` heißt „nichts im Weg".
|
|
*/
|
|
public static function blocker(Host $host): ?string
|
|
{
|
|
// Ohne Schlüssel kann die Brücke sich nicht anmelden, und der Betreiber
|
|
// sähe nur einen Vorspann, der nie weicht.
|
|
if (blank((string) app(SecretVault::class)->get('ssh.private_key'))) {
|
|
return 'no_key';
|
|
}
|
|
|
|
// Derselbe Gedanke, für die beiden anderen Felder, ohne die keine
|
|
// sichere Sitzung entstehen kann. Eine fehlende Tunneladresse ergibt
|
|
// bloß ein Fenster, das nie verbindet — ärgerlich, aber harmlos. Ein
|
|
// fehlender Fingerabdruck ist das Gegenteil: er übergäbe die Prüfung an
|
|
// Code, der noch gar nicht existiert, und dessen naheliegendste Fassung
|
|
// ("kein Fingerabdruck im Ticket → nicht prüfen") eine ungepinnte
|
|
// Root-SSH-Sitzung im Tunnel wäre — genau das, was die Spec mit
|
|
// „Fingerabdruck geprüft" ausdrücklich ausschließt. Die Prüfung gehört
|
|
// hierher, wo der Host ohnehin schon feststeht, nicht in einen
|
|
// Container, der dem Ticket nur noch glauben kann.
|
|
if (blank($host->wg_ip) || blank($host->ssh_host_key)) {
|
|
return 'not_ready';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public static function issue(Host $host, Operator $for): string
|
|
{
|
|
// Die Prüfung steht in `blocker()`; hier bleibt nur, sie ernst zu
|
|
// nehmen. Der Wortlaut ist der von früher — er steht in Logs und in
|
|
// den Tests aus Aufgabe 1.
|
|
match (self::blocker($host)) {
|
|
'no_key' => throw new RuntimeException('Kein SSH-Schlüssel hinterlegt — ohne ihn kann keine Terminalsitzung entstehen.'),
|
|
'not_ready' => throw new RuntimeException("Host {$host->name} hat keine Tunneladresse oder keinen geprüften Fingerabdruck hinterlegt — ohne beides kein Ticket."),
|
|
default => null,
|
|
};
|
|
|
|
$key = (string) app(SecretVault::class)->get('ssh.private_key');
|
|
|
|
return TicketStore::issue([
|
|
'operator_id' => $for->id,
|
|
'host_uuid' => $host->uuid,
|
|
// Die Tunneladresse. Die öffentliche IP wäre der Weg, den
|
|
// SecureHostFirewall ausdrücklich zumacht.
|
|
'ip' => $host->wg_ip,
|
|
'user' => 'root',
|
|
'private_key' => $key,
|
|
'fingerprint' => $host->ssh_host_key,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* `ip` und `fingerprint` sind hier nicht optional: issue() weist beide
|
|
* zurück, bevor überhaupt etwas in Redis landet.
|
|
*
|
|
* @return array{operator_id: int, host_uuid: string, ip: string, user: string, private_key: string, fingerprint: string}|null
|
|
*/
|
|
public static function redeem(string $ticket): ?array
|
|
{
|
|
/** @var array{operator_id: int, host_uuid: string, ip: string, user: string, private_key: string, fingerprint: string}|null */
|
|
return TicketStore::redeem($ticket);
|
|
}
|
|
}
|