Squelette Symfony, entités Server et Action
This commit is contained in:
commit
52ecb4204c
46 changed files with 4308 additions and 0 deletions
471
src/Controller/ServerController.php
Normal file
471
src/Controller/ServerController.php
Normal file
|
|
@ -0,0 +1,471 @@
|
|||
<?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.');
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue