CluPilotCloud/app/Provisioning/Jobs/IssueInstanceAdminAccess.php

115 lines
4.2 KiB
PHP

<?php
namespace App\Provisioning\Jobs;
use App\Models\Instance;
use App\Services\Proxmox\ProxmoxClient;
use App\Services\Wireguard\ConfigHandoff;
use App\Support\NextcloudOcc;
use App\Support\ProvisioningSettings;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Throwable;
/**
* Hands an operator working administrator credentials for a customer's
* Nextcloud.
*
* Nextcloud has no "log in as admin without a password", so this does the only
* honest thing stock Nextcloud allows: it rotates OUR managed admin account's
* password and hands the new one over once. The customer's own accounts are
* untouched.
*
* Runs on the provisioning queue because that worker owns the tunnel and the
* Proxmox credentials; the console has neither.
*/
class IssueInstanceAdminAccess implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 1;
public function __construct(
public string $instanceUuid,
/** Where to leave the credentials for the console to pick up once. */
public string $handoffToken,
public int $requestedBy,
) {
$this->onConnection('provisioning');
$this->onQueue('provisioning');
}
/** Anything that escapes handle() still has to reach the waiting console. */
public function failed(?Throwable $e): void
{
ConfigHandoff::put(json_encode(['error' => 'failed']), $this->handoffToken);
}
public function handle(ProxmoxClient $proxmox): void
{
$instance = Instance::query()->with('host')->where('uuid', $this->instanceUuid)->first();
// Only a live instance: a closed one's VMID can have been reused on the
// same host, and the reset would then land on a stranger's VM.
if ($instance === null || $instance->status !== 'active'
|| $instance->host === null || blank($instance->vmid) || blank($instance->nc_admin_ref)) {
ConfigHandoff::put(json_encode(['error' => 'unavailable']), $this->handoffToken);
return;
}
$username = $instance->nc_admin_ref;
$password = Str::password(24, symbols: false);
try {
$result = $proxmox->forHost($instance->host)->guestExec(
$instance->host->node ?? 'pve',
(int) $instance->vmid,
NextcloudOcc::command(
'user:resetpassword --password-from-env '.escapeshellarg($username),
['OC_PASS' => $password],
),
);
} catch (Throwable $e) {
// An unreachable Proxmox or guest agent throws rather than
// returning a code. Without this the console would poll forever
// instead of saying what happened.
Log::warning('instance admin access errored', [
'instance' => $instance->uuid, 'error' => $e->getMessage(),
]);
ConfigHandoff::put(json_encode(['error' => 'failed']), $this->handoffToken);
return;
}
if ((int) ($result['exitcode'] ?? 1) !== 0) {
Log::warning('instance admin access failed', [
'instance' => $instance->uuid, 'user' => $this->requestedBy,
]);
ConfigHandoff::put(json_encode(['error' => 'failed']), $this->handoffToken);
return;
}
// Auditable on its own: who asked, for which instance, when. The
// credentials themselves are never logged.
Log::info('instance admin access issued', [
'instance' => $instance->uuid, 'user' => $this->requestedBy,
]);
ConfigHandoff::put(json_encode([
// Through address(), which serves the customer's own domain only
// once its DNS proof has been read. A link to an unverified
// domain would not resolve to this instance anyway.
'url' => 'https://'.$instance->address(ProvisioningSettings::dnsZone()),
'username' => $username,
'password' => $password,
]), $this->handoffToken);
}
}