471 lines
19 KiB
PHP
471 lines
19 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @file ServerController.php
|
|
* @brief Controleur principal pour la gestion des serveurs Linux distants
|
|
*
|
|
* Gere toutes les operations sur les serveurs : CRUD, actions SSH sur les services,
|
|
* terminal interactif, gestion des acces operateurs et des services systemd.
|
|
* Toutes les routes necesite au minimum ROLE_OPERATOR, certaines ROLE_ADMIN.
|
|
*
|
|
* @package App\Controller
|
|
* @author Yasser
|
|
*/
|
|
|
|
namespace App\Controller;
|
|
|
|
use App\Entity\Server;
|
|
use App\Entity\ServerAccess;
|
|
use App\Entity\Service;
|
|
use App\Form\ServerType;
|
|
use App\Repository\ServerAccessRepository;
|
|
use App\Repository\ServerRepository;
|
|
use App\Repository\UserRepository;
|
|
use App\Service\SshCommandService;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use Knp\Component\Pager\PaginatorInterface;
|
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\Routing\Attribute\Route;
|
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
|
|
|
/**
|
|
* @class ServerController
|
|
* @brief Controller principal de l'application pour la gestion des serveurs
|
|
*
|
|
* Regroupe toutes les actions liées aux serveurs. Les admins voient tout les
|
|
* serveurs, les opérateurs voient uniquement ceux auxquels ils ont acces.
|
|
* Les actions SSH (start/stop/restart) sont loggées via SshCommandService.
|
|
*/
|
|
#[Route('/server')]
|
|
#[IsGranted('ROLE_OPERATOR')]
|
|
class ServerController extends AbstractController
|
|
{
|
|
/**
|
|
* @brief Liste paginée des serveurs avec filtres de recherche
|
|
*
|
|
* Les admins voient tout les serveurs, les opérateurs seulement
|
|
* ceux qui leurs sont assignés via ServerAccess. La pagination
|
|
* est de 10 elements par page.
|
|
*
|
|
* @param Request $request Requete avec les parametres de filtre (search, status, page)
|
|
* @param ServerRepository $serverRepo
|
|
* @param PaginatorInterface $paginator KnpPaginator
|
|
* @return Response La vue avec la liste des serveurs
|
|
*/
|
|
#[Route('/', name: 'app_server_index')]
|
|
public function index(Request $request, ServerRepository $serverRepo, PaginatorInterface $paginator): Response
|
|
{
|
|
$search = $request->query->get('search');
|
|
$status = $request->query->get('status');
|
|
|
|
$currentUser = $this->getUser();
|
|
$query = $serverRepo->findByFilters($search, $status, $currentUser)->getQuery();
|
|
$servers = $paginator->paginate($query, $request->query->getInt('page', 1), 10);
|
|
|
|
return $this->render('server/index.html.twig', [
|
|
'servers' => $servers,
|
|
'search' => $search,
|
|
'status' => $status,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @brief Formulaire de creation d'un nouveau serveur
|
|
*
|
|
* Apres la creation, on scan automatiquement les services systemd actifs
|
|
* sur le serveur via SSH et on les enregistre en base de donnée.
|
|
* Si le scan echoue (SSH KO) le serveur est quand meme crée.
|
|
*
|
|
* @param Request $request
|
|
* @param EntityManagerInterface $em
|
|
* @param SshCommandService $sshService Pour le scan automatique des services
|
|
* @return Response Le formulaire ou redirection apres succes
|
|
*/
|
|
#[Route('/new', name: 'app_server_new')]
|
|
#[IsGranted('ROLE_ADMIN')]
|
|
public function new(Request $request, EntityManagerInterface $em, SshCommandService $sshService): Response
|
|
{
|
|
$server = new Server();
|
|
$form = $this->createForm(ServerType::class, $server);
|
|
$form->handleRequest($request);
|
|
|
|
if ($form->isSubmitted() && $form->isValid()) {
|
|
$server->setOwner($this->getUser());
|
|
$em->persist($server);
|
|
$em->flush();
|
|
|
|
// scan des services actifs sur le serveur apres l'ajout
|
|
foreach ($sshService->scanServices($server) as $serviceName) {
|
|
$service = new Service();
|
|
$service->setName($serviceName);
|
|
$service->setDisplayName(ucfirst($serviceName));
|
|
$service->setServer($server);
|
|
$em->persist($service);
|
|
}
|
|
$em->flush();
|
|
|
|
$this->addFlash('success', 'Serveur ajouté avec succès.');
|
|
return $this->redirectToRoute('app_server_index');
|
|
}
|
|
|
|
return $this->render('server/new.html.twig', ['form' => $form]);
|
|
}
|
|
|
|
/**
|
|
* @brief Page de detail d'un serveur avec ses services et acces
|
|
*
|
|
* Verifie que l'utilisateur courant a bien acces a ce serveur
|
|
* via assertServerAccess(). Passe aussi la liste des operateurs
|
|
* qui peuvent etre assignés (ceux pas encore assignés).
|
|
*
|
|
* @param Server $server Le serveur a afficher (resolu par ParamConverter)
|
|
* @param UserRepository $userRepo Pour trouver les operateurs assignables
|
|
* @return Response La page de detail du serveur
|
|
*/
|
|
#[Route('/{id}', name: 'app_server_show', requirements: ['id' => '\d+'])]
|
|
public function show(Server $server, UserRepository $userRepo): Response
|
|
{
|
|
$this->assertServerAccess($server);
|
|
|
|
$assignedIds = $server->getServerAccesses()->map(fn($a) => $a->getUser()->getId())->toArray();
|
|
$assignableUsers = $userRepo->findOperatorsExcluding($assignedIds);
|
|
|
|
return $this->render('server/show.html.twig', [
|
|
'server' => $server,
|
|
'assignableUsers' => $assignableUsers,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @brief Formulaire d'edition d'un serveur
|
|
*
|
|
* Si le champ mot de passe SSH est laissé vide, on fait un refresh()
|
|
* pour pas ecraser le mot de passe existant en base.
|
|
*
|
|
* @param Request $request
|
|
* @param Server $server Le serveur a modifier
|
|
* @param EntityManagerInterface $em
|
|
* @return Response Le formulaire d'edition ou redirection
|
|
*/
|
|
#[Route('/{id}/edit', name: 'app_server_edit', requirements: ['id' => '\d+'])]
|
|
#[IsGranted('ROLE_ADMIN')]
|
|
public function edit(Request $request, Server $server, EntityManagerInterface $em): Response
|
|
{
|
|
$form = $this->createForm(ServerType::class, $server);
|
|
$form->handleRequest($request);
|
|
|
|
if ($form->isSubmitted() && $form->isValid()) {
|
|
// si le mdp est vide on recharge l'anciene valeur pour pas l'effacer
|
|
if (!$form->get('sshPassword')->getData()) {
|
|
$em->refresh($server);
|
|
}
|
|
$em->flush();
|
|
$this->addFlash('success', 'Server updated successfully.');
|
|
return $this->redirectToRoute('app_server_show', ['id' => $server->getId()]);
|
|
}
|
|
|
|
return $this->render('server/edit.html.twig', ['form' => $form, 'server' => $server]);
|
|
}
|
|
|
|
/**
|
|
* @brief Suppression d'un serveur
|
|
*
|
|
* La suppression est en cascade sur les services, logs et acces associés
|
|
* (configuré dans les entités). Le token CSRF est verifié avant suppression.
|
|
*
|
|
* @param Request $request
|
|
* @param Server $server Le serveur a supprimer
|
|
* @param EntityManagerInterface $em
|
|
* @return Response Redirection vers la liste des serveurs
|
|
*/
|
|
#[Route('/{id}/delete', name: 'app_server_delete', methods: ['POST'], requirements: ['id' => '\d+'])]
|
|
#[IsGranted('ROLE_ADMIN')]
|
|
public function delete(Request $request, Server $server, EntityManagerInterface $em): Response
|
|
{
|
|
if ($this->isCsrfTokenValid('delete_server_' . $server->getId(), $request->request->get('_token'))) {
|
|
$em->remove($server);
|
|
$em->flush();
|
|
$this->addFlash('success', 'Server deleted.');
|
|
}
|
|
|
|
return $this->redirectToRoute('app_server_index');
|
|
}
|
|
|
|
/**
|
|
* @brief Execute une commande libre via le terminal SSH interactif
|
|
*
|
|
* Recoit une commande en POST et la transmet via SshCommandService.
|
|
* Si la commande contient 'sudo' et que le mot de passe est necessaire,
|
|
* retourne needs_sudo=true pour que le front affiche la modal de saisie.
|
|
*
|
|
* @param Request $request Requete avec 'command', optionnellement 'sudo_password'
|
|
* @param Server $server Le serveur cible
|
|
* @param SshCommandService $sshService
|
|
* @return JsonResponse Resultat de la commande avec output et exit_code
|
|
*/
|
|
#[Route('/{id}/terminal', name: 'app_server_terminal', methods: ['POST'], requirements: ['id' => '\d+'])]
|
|
public function terminal(Request $request, Server $server, SshCommandService $sshService): JsonResponse
|
|
{
|
|
$this->assertServerAccess($server);
|
|
|
|
if (!$this->isCsrfTokenValid('terminal_' . $server->getId(), $request->request->get('_token'))) {
|
|
return $this->json(['error' => 'Invalid CSRF token'], 403);
|
|
}
|
|
|
|
$command = trim($request->request->get('command', ''));
|
|
if ($command === '') {
|
|
return $this->json(['success' => true, 'output' => '', 'needs_sudo' => false, 'exit_code' => 0]);
|
|
}
|
|
|
|
$sudoPassword = $request->request->get('sudo_password') ?: null;
|
|
$currentUser = $this->getUser();
|
|
$result = $sshService->runCommand($server, $command, $sudoPassword, $currentUser);
|
|
|
|
return $this->json($result);
|
|
}
|
|
|
|
/**
|
|
* @brief Execute une action systemd (start/stop/restart/status) sur un service
|
|
*
|
|
* L'action et le nom du service sont passés en POST. L'action est
|
|
* loggée dans ActionLog peu importe le resultat (succes ou echec).
|
|
* Retourne needs_sudo si le mot de passe sudo est requis.
|
|
*
|
|
* @param Request $request Requete avec 'action', 'service', optionnellement 'sudo_password'
|
|
* @param Server $server Le serveur cible
|
|
* @param SshCommandService $sshService
|
|
* @return JsonResponse Statut, output et needs_sudo
|
|
*/
|
|
#[Route('/{id}/action', name: 'app_server_action', methods: ['POST'], requirements: ['id' => '\d+'])]
|
|
public function action(Request $request, Server $server, SshCommandService $sshService): JsonResponse
|
|
{
|
|
$this->assertServerAccess($server);
|
|
|
|
if (!$this->isCsrfTokenValid('server_action', $request->request->get('_token'))) {
|
|
return $this->json(['error' => 'Invalid CSRF token'], 403);
|
|
}
|
|
|
|
$action = $request->request->get('action');
|
|
$serviceName = $request->request->get('service');
|
|
|
|
if (!$action || !$serviceName) {
|
|
return $this->json(['error' => 'Missing parameters'], 400);
|
|
}
|
|
|
|
$sudoPassword = $request->request->get('sudo_password') ?: null;
|
|
$log = $sshService->executeAction($server, $action, $serviceName, $this->getUser(), $sudoPassword);
|
|
|
|
return $this->json([
|
|
'status' => $log->getStatus(),
|
|
'output' => $log->getOutput(),
|
|
'needs_sudo' => $log->getStatus() === 'needs_sudo',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @brief Verifie le statut (active/inactive) de tout les services d'un serveur
|
|
*
|
|
* Utilisé par le front en AJAX pour mettre a jour les badges de statut
|
|
* sur la page de detail du serveur sans recharger la page.
|
|
*
|
|
* @param Server $server
|
|
* @param SshCommandService $sshService
|
|
* @return JsonResponse Tableau associatif nom_service => 'active'|'inactive'
|
|
*/
|
|
#[Route('/{id}/services/status', name: 'app_services_status', methods: ['GET'], requirements: ['id' => '\d+'])]
|
|
public function servicesStatus(Server $server, SshCommandService $sshService): JsonResponse
|
|
{
|
|
$this->assertServerAccess($server);
|
|
$names = $server->getServices()->map(fn($s) => $s->getName())->toArray();
|
|
if (empty($names)) {
|
|
return $this->json([]);
|
|
}
|
|
return $this->json($sshService->checkServicesStatus($server, $names));
|
|
}
|
|
|
|
/**
|
|
* @brief Verifie si le serveur est accessible via SSH et met a jour son statut
|
|
*
|
|
* Tente une connexion SSH avec un timeout court. Met a jour lastStatus
|
|
* en base et retourne 'online' ou 'offline'.
|
|
*
|
|
* @param Server $server
|
|
* @param SshCommandService $sshService
|
|
* @param EntityManagerInterface $em Pour persister le nouveau statut
|
|
* @return JsonResponse ['status' => 'online'|'offline']
|
|
*/
|
|
#[Route('/{id}/status', name: 'app_server_status', methods: ['GET'], requirements: ['id' => '\d+'])]
|
|
public function status(Server $server, SshCommandService $sshService, EntityManagerInterface $em): JsonResponse
|
|
{
|
|
$this->assertServerAccess($server);
|
|
|
|
$status = $sshService->checkStatus($server);
|
|
$server->setLastStatus($status);
|
|
$em->flush();
|
|
|
|
return $this->json(['status' => $status]);
|
|
}
|
|
|
|
/**
|
|
* @brief Ajoute un acces operateur a un serveur depuis la page du serveur
|
|
*
|
|
* Cree un ServerAccess pour l'utilisateur selectionné avec les credentials SSH
|
|
* fournis. Double check que l'acces n'existe pas deja avant de creer.
|
|
*
|
|
* @param Request $request
|
|
* @param int $serverId
|
|
* @param EntityManagerInterface $em
|
|
* @param ServerRepository $serverRepo
|
|
* @param UserRepository $userRepo
|
|
* @param ServerAccessRepository $accessRepo
|
|
* @return Response Redirection vers la page du serveur
|
|
*/
|
|
#[Route('/{serverId}/access/new', name: 'app_access_new', methods: ['POST'])]
|
|
#[IsGranted('ROLE_ADMIN')]
|
|
public function addAccess(Request $request, int $serverId, EntityManagerInterface $em, ServerRepository $serverRepo, UserRepository $userRepo, ServerAccessRepository $accessRepo): Response
|
|
{
|
|
$server = $serverRepo->find($serverId);
|
|
if (!$server) {
|
|
throw $this->createNotFoundException();
|
|
}
|
|
|
|
if ($this->isCsrfTokenValid('add_access_' . $serverId, $request->request->get('_token'))) {
|
|
$user = $userRepo->find($request->request->get('user_id'));
|
|
if ($user && !$accessRepo->findOneBy(['server' => $server, 'user' => $user])) {
|
|
$access = new ServerAccess();
|
|
$access->setServer($server);
|
|
$access->setUser($user);
|
|
$access->setSshUser($request->request->get('ssh_user'));
|
|
$access->setSshPassword($request->request->get('ssh_password'));
|
|
$em->persist($access);
|
|
$em->flush();
|
|
$this->addFlash('success', 'Access granted to ' . $user->getPseudo());
|
|
}
|
|
}
|
|
|
|
return $this->redirectToRoute('app_server_show', ['id' => $serverId]);
|
|
}
|
|
|
|
/**
|
|
* @brief Supprime un acces operateur depuis la page du serveur
|
|
*
|
|
* @param Request $request
|
|
* @param int $serverId Id du serveur (pour verifier la cohérence)
|
|
* @param int $accessId Id du ServerAccess a supprimer
|
|
* @param EntityManagerInterface $em
|
|
* @param ServerAccessRepository $accessRepo
|
|
* @return Response Redirection vers la page du serveur
|
|
*/
|
|
#[Route('/{serverId}/access/{accessId}/delete', name: 'app_access_delete', methods: ['POST'])]
|
|
#[IsGranted('ROLE_ADMIN')]
|
|
public function deleteAccess(Request $request, int $serverId, int $accessId, EntityManagerInterface $em, ServerAccessRepository $accessRepo): Response
|
|
{
|
|
$access = $accessRepo->find($accessId);
|
|
if ($access && $access->getServer()->getId() === $serverId
|
|
&& $this->isCsrfTokenValid('delete_access_' . $accessId, $request->request->get('_token'))) {
|
|
$em->remove($access);
|
|
$em->flush();
|
|
$this->addFlash('success', 'Access revoked.');
|
|
}
|
|
|
|
return $this->redirectToRoute('app_server_show', ['id' => $serverId]);
|
|
}
|
|
|
|
/**
|
|
* @brief Supprime un service systemd de la liste surveillée
|
|
*
|
|
* Ne supprime pas le service sur le serveur Linux, juste
|
|
* l'enregistrement en base qui indique qu'on le surveille.
|
|
*
|
|
* @param Request $request
|
|
* @param int $serverId
|
|
* @param int $serviceId Id du Service a supprimer
|
|
* @param EntityManagerInterface $em
|
|
* @param ServerRepository $serverRepo
|
|
* @return Response Redirection vers la page du serveur
|
|
*/
|
|
#[Route('/{serverId}/service/{serviceId}/delete', name: 'app_service_delete', methods: ['POST'])]
|
|
#[IsGranted('ROLE_ADMIN')]
|
|
public function deleteService(Request $request, int $serverId, int $serviceId, EntityManagerInterface $em, ServerRepository $serverRepo): Response
|
|
{
|
|
$server = $serverRepo->find($serverId);
|
|
if (!$server) {
|
|
throw $this->createNotFoundException();
|
|
}
|
|
|
|
if ($this->isCsrfTokenValid('delete_service_' . $serviceId, $request->request->get('_token'))) {
|
|
foreach ($server->getServices() as $service) {
|
|
if ($service->getId() === $serviceId) {
|
|
$em->remove($service);
|
|
$em->flush();
|
|
$this->addFlash('success', 'Service deleted.');
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $this->redirectToRoute('app_server_show', ['id' => $serverId]);
|
|
}
|
|
|
|
/**
|
|
* @brief Ajoute manuellement un service systemd a surveiller
|
|
*
|
|
* L'admin peut ajouter un service qui n'a pas été detecté automatiquement
|
|
* lors du scan initial ou qui a été installé apres l'ajout du serveur.
|
|
*
|
|
* @param Request $request
|
|
* @param int $serverId
|
|
* @param EntityManagerInterface $em
|
|
* @param ServerRepository $serverRepo
|
|
* @return Response Redirection vers la page du serveur
|
|
*/
|
|
#[Route('/{serverId}/service/new', name: 'app_service_new', methods: ['POST'])]
|
|
#[IsGranted('ROLE_ADMIN')]
|
|
public function addService(Request $request, int $serverId, EntityManagerInterface $em, ServerRepository $serverRepo): Response
|
|
{
|
|
$server = $serverRepo->find($serverId);
|
|
if (!$server) {
|
|
throw $this->createNotFoundException();
|
|
}
|
|
|
|
if ($this->isCsrfTokenValid('add_service_' . $serverId, $request->request->get('_token'))) {
|
|
$service = new Service();
|
|
$service->setName($request->request->get('name'));
|
|
$service->setDisplayName($request->request->get('display_name'));
|
|
$service->setServer($server);
|
|
$em->persist($service);
|
|
$em->flush();
|
|
$this->addFlash('success', 'Service added.');
|
|
}
|
|
|
|
return $this->redirectToRoute('app_server_show', ['id' => $serverId]);
|
|
}
|
|
|
|
/**
|
|
* @brief Verifie que l'utilisateur courant a bien acces a ce serveur
|
|
*
|
|
* Les admins ont acces a tout les serveurs. Pour les opérateurs,
|
|
* on verifie qu'il existe un ServerAccess pour cet user et ce serveur.
|
|
* Lance une AccessDeniedException si l'acces est refusé.
|
|
*
|
|
* @param Server $server Le serveur dont on verifie l'acces
|
|
* @throws \Symfony\Component\Security\Core\Exception\AccessDeniedException
|
|
*/
|
|
private function assertServerAccess(Server $server): void
|
|
{
|
|
if ($this->isGranted('ROLE_ADMIN')) {
|
|
return;
|
|
}
|
|
|
|
foreach ($server->getServerAccesses() as $access) {
|
|
if ($access->getUser() === $this->getUser()) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
throw $this->createAccessDeniedException('You do not have access to this server.');
|
|
}
|
|
}
|