110 lines
4.0 KiB
PHP
110 lines
4.0 KiB
PHP
<?php
|
|
|
|
namespace App\Provisioning\Jobs;
|
|
|
|
use App\Models\Instance;
|
|
use App\Services\Proxmox\ProxmoxClient;
|
|
use App\Services\Wireguard\ConfigHandoff;
|
|
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,
|
|
'cd /opt/nextcloud && OC_PASS='.escapeshellarg($password).
|
|
' docker compose exec -T -e OC_PASS app php occ user:resetpassword --password-from-env '.
|
|
escapeshellarg($username),
|
|
);
|
|
} 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([
|
|
'url' => 'https://'.($instance->custom_domain ?: $instance->subdomain.'.'.ProvisioningSettings::dnsZone()),
|
|
'username' => $username,
|
|
'password' => $password,
|
|
]), $this->handoffToken);
|
|
}
|
|
}
|