183 lines
7.4 KiB
PHP
183 lines
7.4 KiB
PHP
<?php
|
|
|
|
namespace App\Provisioning\Jobs;
|
|
|
|
use App\Models\Instance;
|
|
use App\Models\Seat;
|
|
use App\Services\Nextcloud\NextcloudUsers;
|
|
use Illuminate\Bus\Queueable;
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
use Illuminate\Foundation\Bus\Dispatchable;
|
|
use Illuminate\Queue\InteractsWithQueue;
|
|
use Illuminate\Queue\SerializesModels;
|
|
use Throwable;
|
|
|
|
/**
|
|
* Spiegelt einen Sitz in die Nextcloud des Kunden.
|
|
*
|
|
* Läuft auf der `provisioning`-Warteschlange, weil nur dieser Arbeiter den
|
|
* Tunnel und die Proxmox-Zugangsdaten hat — das Portal hat weder das eine noch
|
|
* das andere und darf es auch gar nicht versuchen.
|
|
*
|
|
* Daraus folgt: der Knopf wirkt nicht sofort. Deshalb schreibt dieser Auftrag
|
|
* zurück, was WIRKLICH passiert ist (`nc_state`, `nc_error`) — sonst drückt
|
|
* der Inhaber wieder und wieder, weil nichts sichtbar geschieht. Genau die
|
|
* Beschwerde, die dieses Projekt schon zweimal hatte.
|
|
*
|
|
* `tries = 1`: ein blinder Wiederholungslauf gegen einen nicht erreichbaren
|
|
* Gast bringt nichts, und der Inhaber hat einen Knopf dafür.
|
|
*/
|
|
class SyncSeatToNextcloud implements ShouldQueue
|
|
{
|
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
|
|
public int $tries = 1;
|
|
|
|
public function __construct(
|
|
public string $seatUuid,
|
|
/** invite | role | disable | enable | restore */
|
|
public string $action,
|
|
) {
|
|
$this->onConnection('provisioning');
|
|
$this->onQueue('provisioning');
|
|
}
|
|
|
|
/** Auch was an handle() vorbeifliegt, muss am Sitz sichtbar werden. */
|
|
public function failed(?Throwable $e): void
|
|
{
|
|
Seat::query()->where('uuid', $this->seatUuid)->update([
|
|
'nc_state' => Seat::STATE_FAILED,
|
|
'nc_error' => 'unexpected',
|
|
]);
|
|
}
|
|
|
|
public function handle(NextcloudUsers $users): void
|
|
{
|
|
$seat = Seat::query()->with('customer')->where('uuid', $this->seatUuid)->first();
|
|
|
|
if ($seat === null) {
|
|
return;
|
|
}
|
|
|
|
// Erneut geprüft, nicht der Seite geglaubt: eine geschlossene Instanz
|
|
// kann ihre VMID auf demselben Host weiterverliehen haben — dieselbe
|
|
// Falle, die in IssueInstanceAdminAccess schon beschrieben steht.
|
|
$instance = $seat->customer?->instances()
|
|
->whereIn('status', ['active', 'cancellation_scheduled'])
|
|
->latest('id')->first();
|
|
|
|
if ($instance === null || $instance->host === null || blank($instance->vmid)) {
|
|
$this->record($seat, false, 'no_instance');
|
|
|
|
return;
|
|
}
|
|
|
|
$ok = match ($this->action) {
|
|
'invite' => $this->invite($users, $instance, $seat),
|
|
'role' => $users->applyRole($instance, $seat),
|
|
'disable' => $users->disable($instance, $seat),
|
|
'enable' => $users->enable($instance, $seat),
|
|
// Der Wiederholen-Knopf an einer OFFENEN Zeile. Er weiss nicht,
|
|
// welcher der beiden Befehle beim letzten Mal scheiterte — das
|
|
// stünde nirgends am Sitz, und ein Feld dafür wäre eine
|
|
// weitere Behauptung über die Cloud des Kunden, die irgendwann
|
|
// nicht mehr stimmt. Beide zu schicken ist billiger und ehrlicher
|
|
// als es zu raten; `user:enable` auf ein offenes Konto ist
|
|
// folgenlos, und das Setzen der Rolle ist es ohnehin. Siehe
|
|
// Users::retry().
|
|
'restore' => $users->enable($instance, $seat) && $users->applyRole($instance, $seat),
|
|
default => false,
|
|
};
|
|
|
|
$this->record($seat, $ok, $ok ? null : 'guest_failed');
|
|
}
|
|
|
|
/**
|
|
* Einladen setzt in einem Zug auch die Gruppe: ein Benutzer, der angelegt
|
|
* ist und in keiner Rolle steckt, sieht in seiner neuen Cloud nichts und
|
|
* meldet sich am ersten Tag beim Inhaber.
|
|
*
|
|
* Das Anlegen wird dabei SOFORT vermerkt, noch bevor die Gruppe gesetzt
|
|
* wird. `nc_synced_at` beantwortet die Frage „gibt es dieses Konto dort
|
|
* überhaupt?", und ab dem geglückten `user:add` lautet die Antwort ja —
|
|
* auch wenn der nächste Befehl scheitert. Die Seite entscheidet genau
|
|
* daran, ob ein späteres Sperren etwas zu sperren hätte; ohne diese Zeile
|
|
* bliebe der gefährlichste Fall offen: Konto angelegt, Rolle gescheitert,
|
|
* Zugang entzogen — und nie eine Sperre losgeschickt.
|
|
*
|
|
* `nc_state` bleibt davon unberührt: der Versuch ist erst gelungen, wenn
|
|
* auch die Rolle sitzt.
|
|
*/
|
|
private function invite(NextcloudUsers $users, Instance $instance, Seat $seat): bool
|
|
{
|
|
// VOR dem Vermerk gelesen: war dieser Sitz schon einmal in der
|
|
// Nextcloud, ist dies eine Wiederaufnahme — und sein Konto dort steht
|
|
// gesperrt, weil `revoke()` ein `disable` geschickt hat. `user:welcome`
|
|
// macht eine Sperre nicht rückgängig; ohne diese Zeile bekäme der
|
|
// Wiederaufgenommene eine Willkommensmail zu einer Tür, die zu bleibt.
|
|
// Bei einem frisch angelegten Konto entfällt der Befehl ganz, statt
|
|
// sich auf ein folgenloses `user:enable` zu verlassen.
|
|
$wiederaufnahme = $seat->nc_synced_at !== null;
|
|
|
|
if (! $users->invite($instance, $seat)) {
|
|
return false;
|
|
}
|
|
|
|
$seat->forceFill(['nc_synced_at' => now()])->save();
|
|
|
|
/*
|
|
* Zwischen Absenden und Ausführen kann sich die Absicht geändert
|
|
* haben — dieselbe Begründung wie beim Vermerk oben.
|
|
*
|
|
* Der Arbeiter teilt sich die Warteschlange mit der bezahlten
|
|
* Bereitstellung; das Fenster ist MINUTEN lang, nicht Millisekunden.
|
|
* In dieser Zeit merkt ein Inhaber seinen Fehlgriff und entzieht. Ohne
|
|
* diese Zeilen führe der Auftrag danach blind aus, was beim Absenden
|
|
* galt: die Zeile zeichnete „Entfernt" und in der Cloud stände ein
|
|
* aktives, freigeschaltetes Konto.
|
|
*
|
|
* Frisch aus der Datenbank, nicht am mitgeschleppten Modell: dieses
|
|
* Objekt stammt vom Anfang des Auftrags und trägt den Status von
|
|
* damals.
|
|
*/
|
|
$gesperrtGewollt = in_array(
|
|
Seat::query()->where('uuid', $seat->uuid)->value('status'),
|
|
['revoked', 'suspended'],
|
|
true,
|
|
);
|
|
|
|
// Eine Tür aufzumachen, um sie im selben Auftrag wieder zuzuziehen,
|
|
// wäre im besten Fall folgenlos und im schlechteren ein offenes
|
|
// Zeitfenster.
|
|
if ($wiederaufnahme && ! $gesperrtGewollt && ! $users->enable($instance, $seat)) {
|
|
return false;
|
|
}
|
|
|
|
$rolleOk = $users->applyRole($instance, $seat);
|
|
|
|
if (! $gesperrtGewollt) {
|
|
return $rolleOk;
|
|
}
|
|
|
|
// Die Sperre geht auch dann noch hinaus, wenn die Rolle scheiterte:
|
|
// an ihr hängt ein beendeter Zugang, und eine misslungene Gruppe darf
|
|
// ihn nicht offen lassen. Gemeldet wird trotzdem beides — die Zeile
|
|
// stimmt nur, wenn sie den Fehlschlag zeigt.
|
|
return $users->disable($instance, $seat) && $rolleOk;
|
|
}
|
|
|
|
/**
|
|
* `nc_synced_at` bleibt beim Fehlschlag stehen, statt geleert zu werden:
|
|
* wann dieser Sitz zuletzt WIRKLICH stimmte, ist genau die Angabe, die
|
|
* einen Fehlschlag einordnet — heute Morgen oder vor drei Wochen.
|
|
*/
|
|
private function record(Seat $seat, bool $ok, ?string $grund): void
|
|
{
|
|
$seat->forceFill([
|
|
'nc_state' => $ok ? Seat::STATE_SYNCED : Seat::STATE_FAILED,
|
|
'nc_error' => $grund,
|
|
'nc_synced_at' => $ok ? now() : $seat->nc_synced_at,
|
|
])->save();
|
|
}
|
|
}
|