feat: validate required SSH permissions before creating server
- New server_permissions table (migration 007) stores required SSH permissions per server - PermissionValidator service connects via SSH and validates each permission before save - ServerPermission model for CRUD on server_permissions table - Web flow: create/edit forms include dynamic permission rows with quick-add presets - API flow: serverAdd/serverUpdate accept permissions array, return 400 on failure - Show view displays required permissions on server detail page - Form input preserved on validation failure (credentials excluded) - AGENTS.md updated with new conventions
This commit is contained in:
102
src/Services/PermissionValidator.php
Normal file
102
src/Services/PermissionValidator.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ServerManager\Services;
|
||||
|
||||
class PermissionValidator
|
||||
{
|
||||
private SSHService $ssh;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->ssh = new SSHService();
|
||||
}
|
||||
|
||||
public function validate(array $serverConfig, array $permissions): array
|
||||
{
|
||||
try {
|
||||
if (!$this->ssh->connect($serverConfig, true)) {
|
||||
return [
|
||||
'passed' => false,
|
||||
'results' => [],
|
||||
'error' => 'Could not connect to the remote server. Please check the connection details.',
|
||||
];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return [
|
||||
'passed' => false,
|
||||
'results' => [],
|
||||
'error' => 'SSH connection failed: ' . $e->getMessage(),
|
||||
];
|
||||
}
|
||||
|
||||
$results = [];
|
||||
|
||||
foreach ($permissions as $index => $perm) {
|
||||
if (empty($perm['type']) || !isset($perm['value'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$type = $perm['type'];
|
||||
$value = $perm['value'];
|
||||
$description = $perm['description'] ?? '';
|
||||
|
||||
$command = $this->buildCheckCommand($type, $value);
|
||||
|
||||
try {
|
||||
$output = $this->ssh->exec($command);
|
||||
$passed = $this->evaluateResult($type, $output);
|
||||
$results[] = [
|
||||
'type' => $type,
|
||||
'value' => $value,
|
||||
'description' => $description,
|
||||
'passed' => $passed,
|
||||
'message' => $passed ? 'Permission verified' : ($type === 'sudo' ? 'Passwordless sudo is not available' : 'Permission not granted: ' . trim($output['output'] ?? '')),
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
$results[] = [
|
||||
'type' => $type,
|
||||
'value' => $value,
|
||||
'description' => $description,
|
||||
'passed' => false,
|
||||
'message' => 'Check failed: ' . $e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$this->ssh->disconnect();
|
||||
|
||||
$allPassed = !empty($results) && empty(array_filter($results, fn($r) => !$r['passed']));
|
||||
|
||||
return [
|
||||
'passed' => $allPassed,
|
||||
'results' => $results,
|
||||
'error' => null,
|
||||
];
|
||||
}
|
||||
|
||||
private function buildCheckCommand(string $type, string $value): string
|
||||
{
|
||||
return match ($type) {
|
||||
'sudo' => 'sudo -n true 2>&1 && echo "OK" || echo "FAIL"',
|
||||
'command' => sprintf('command -v %s 2>/dev/null && echo "OK" || echo "FAIL"', escapeshellarg($value)),
|
||||
'file_read' => sprintf('test -r %s 2>/dev/null && echo "OK" || echo "FAIL"', escapeshellarg($value)),
|
||||
'file_write' => sprintf('test -w %s 2>/dev/null && echo "OK" || echo "FAIL"', escapeshellarg($value)),
|
||||
'custom' => sprintf('%s 2>&1; echo "EXIT:$?"', $value),
|
||||
default => 'echo "FAIL"',
|
||||
};
|
||||
}
|
||||
|
||||
private function evaluateResult(string $type, array $output): bool
|
||||
{
|
||||
$exitCode = $output['exit_status'] ?? 1;
|
||||
$stdout = trim($output['output'] ?? '');
|
||||
|
||||
if ($type === 'custom') {
|
||||
return $exitCode === 0;
|
||||
}
|
||||
|
||||
return $exitCode === 0 && str_contains($stdout, 'OK');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user