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