nc_username; if (! $this->isWellFormed($user, $seat)) { return false; } return $this->run($instance, function ($pve, $node, $vmid) use ($user, $seat) { $vorhanden = (int) ($pve->guestExec( $node, $vmid, NextcloudOcc::command('user:info '.escapeshellarg($user)) )['exitcode'] ?? 1) === 0; // Wiederholbar nach einem Absturz: ein zweiter Lauf legt keinen // zweiten Benutzer an, sondern schickt die Willkommensmail erneut. // Genau wie CreateCustomerAdmin es tut. return $vorhanden ? ['user:welcome --reset-password '.escapeshellarg($user)] : [ 'user:add --generate-password' .' --email='.escapeshellarg((string) $seat->email) .' --display-name='.escapeshellarg((string) ($seat->name ?: $seat->email)) .' --group='.escapeshellarg(Seat::GROUPS[$seat->role] ?? 'mitarbeiter') .' '.escapeshellarg($user), ]; }); } /** Gruppe setzen — und bei readonly der Speicherplatz. */ public function applyRole(Instance $instance, Seat $seat): bool { $user = (string) $seat->nc_username; if (! $this->isWellFormed($user, $seat)) { return false; } $ziel = Seat::GROUPS[$seat->role] ?? 'mitarbeiter'; return $this->run($instance, function ($pve, $node, $vmid) use ($user, $seat, $ziel) { $befehle = []; // Aus jeder anderen bekannten Gruppe heraus, in die eine hinein. foreach (array_unique(array_values(Seat::GROUPS)) as $gruppe) { if ($gruppe !== $ziel) { $befehle[] = 'group:removeuser '.escapeshellarg($gruppe).' '.escapeshellarg($user); } } $befehle[] = 'group:adduser '.escapeshellarg($ziel).' '.escapeshellarg($user); // Der Speicherplatz. Siehe ApplyStorageQuota: ein Konto mit // EIGENEM Wert folgt der Vorgabe der Instanz nicht mehr. Fuer // readonly ist genau das gewollt; beim VERLASSEN der Rolle muss // der eigene Wert deshalb WEG, nicht ueberschrieben werden. $befehle[] = $seat->isReadonly() ? 'user:setting '.escapeshellarg($user).' files quota '.escapeshellarg('0 B') : 'user:setting '.escapeshellarg($user).' files quota --delete'; return $befehle; }); } public function disable(Instance $instance, Seat $seat): bool { $user = (string) $seat->nc_username; if (! $this->isWellFormed($user, $seat)) { return false; } return $this->run($instance, fn ($pve, $node, $vmid) => [ 'user:disable '.escapeshellarg($user), // user:disable allein laesst laufende Sitzungen bis zu fuenf // Minuten weiterleben. Bei jemandem, der gerade gegangen ist, // sind fuenf Minuten fuenf zu viel. 'user:auth-tokens:delete '.escapeshellarg($user), ]); } public function enable(Instance $instance, Seat $seat): bool { $user = (string) $seat->nc_username; if (! $this->isWellFormed($user, $seat)) { return false; } return $this->run($instance, fn ($pve, $node, $vmid) => ['user:enable '.escapeshellarg($user)]); } /** * Nextcloud laesst Buchstaben, Ziffern und `-_.@` in Kennungen zu. Alles * andere ist entweder ein Fehler weiter oben oder ein Versuch — beides * will man sehen, und keines darf in eine Shell. */ private function isWellFormed(string $user, Seat $seat): bool { if ($user !== '' && preg_match('/^[A-Za-z0-9._@-]+$/', $user) === 1) { return true; } report(new RuntimeException( "NextcloudUsers: abgewiesene Kennung fuer Sitz [{$seat->uuid}] — nichts ausgefuehrt." )); return false; } /** * Der Verbindungsaufbau steht EINMAL hier, nicht in jeder Methode. Der * Rueckruf bekommt den fertigen Client mit — er braucht ihn, weil `invite()` * erst nachsehen muss, ob es den Benutzer schon gibt, bevor es entscheidet, * welchen Befehl es baut. * * @param callable(ProxmoxClient, string, int): array $bauen */ private function run(Instance $instance, callable $bauen): bool { if ($instance->host === null || blank($instance->vmid)) { return false; } $node = $instance->host->node ?? 'pve'; $vmid = (int) $instance->vmid; try { $pve = $this->pve->forHost($instance->host); $ok = true; foreach ($bauen($pve, $node, $vmid) as $argumente) { $ergebnis = $pve->guestExec($node, $vmid, NextcloudOcc::command($argumente)); $ok = ((int) ($ergebnis['exitcode'] ?? 1) === 0) && $ok; } return $ok; } catch (Throwable $e) { // Ein abgeschalteter Gast wirft, statt einen Fehlercode zu liefern. // Nie mit Zugangsdaten, nie mit Stacktrace an den Kunden. Log::warning('nextcloud user command failed', [ 'instance' => $instance->uuid, 'error' => $e->getMessage(), ]); return false; } } }