- 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
103 lines
3.3 KiB
PHP
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');
|
|
}
|
|
}
|