Files
server-manager/src/Services/PermissionValidator.php
Agent 0cfed5bf27 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
2026-06-13 10:41:18 -04:00

103 lines
3.3 KiB
PHP

<?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');
}
}