Squelette Symfony, entités Server et Action
This commit is contained in:
commit
52ecb4204c
46 changed files with 4308 additions and 0 deletions
0
src/Controller/.gitignore
vendored
Normal file
0
src/Controller/.gitignore
vendored
Normal file
77
src/Controller/ActionLogController.php
Normal file
77
src/Controller/ActionLogController.php
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file ActionLogController.php
|
||||
* @brief Controleur pour la consultation du journal d'audit
|
||||
*
|
||||
* Permet aux opérateurs et admins de consulter l'historique
|
||||
* des actions effectuées sur les serveurs. Les admins voient
|
||||
* tout, les opérateurs voient uniquement leurs propres actions.
|
||||
*
|
||||
* @package App\Controller
|
||||
* @author Yasser
|
||||
*/
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Repository\ActionLogRepository;
|
||||
use App\Repository\ServerRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use Knp\Component\Pager\PaginatorInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
* @class ActionLogController
|
||||
* @brief Affichage et filtrage du journal des actions SSH
|
||||
*
|
||||
* Accessible uniquement aux ROLE_OPERATOR et supérieur.
|
||||
* Propose des filtres par serveur, utilisateur et plage de dates.
|
||||
* La liste des utilisateurs dans les filtres est visible uniquement par les admins.
|
||||
*/
|
||||
#[Route('/logs')]
|
||||
#[IsGranted('ROLE_OPERATOR')]
|
||||
class ActionLogController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @brief Liste paginée des logs avec filtres
|
||||
*
|
||||
* Recupere les logs depuis la base de donnée avec les filtres
|
||||
* optionels passés en parametre GET. La pagination est gérée
|
||||
* par KnpPaginatorBundle avec 20 entrées par page.
|
||||
*
|
||||
* @param Request $request La requete avec les parametres de filtre
|
||||
* @param ActionLogRepository $logRepo Repository pour les logs
|
||||
* @param ServerRepository $serverRepo Pour la liste des serveurs dans les filtres
|
||||
* @param UserRepository $userRepo Pour la liste des utilisateurs (admins seulement)
|
||||
* @param PaginatorInterface $paginator Le paginateur KnpPaginator
|
||||
* @return Response La vue avec les logs paginés et les filtres
|
||||
*/
|
||||
#[Route('/', name: 'app_log_index')]
|
||||
public function index(
|
||||
Request $request,
|
||||
ActionLogRepository $logRepo,
|
||||
ServerRepository $serverRepo,
|
||||
UserRepository $userRepo,
|
||||
PaginatorInterface $paginator
|
||||
): Response {
|
||||
$serverId = $request->query->getInt('server') ?: null;
|
||||
$userId = $request->query->getInt('user') ?: null;
|
||||
$dateFrom = $request->query->get('date_from');
|
||||
$dateTo = $request->query->get('date_to');
|
||||
|
||||
$query = $logRepo->findByFilters($serverId, $userId, $dateFrom, $dateTo)->getQuery();
|
||||
$logs = $paginator->paginate($query, $request->query->getInt('page', 1), 20);
|
||||
|
||||
return $this->render('logs/index.html.twig', [
|
||||
'logs' => $logs,
|
||||
'servers' => $serverRepo->findAll(),
|
||||
// la liste des users dans les filtres est reservée aux admins
|
||||
'users' => $this->isGranted('ROLE_ADMIN') ? $userRepo->findAll() : [],
|
||||
'filters' => compact('serverId', 'userId', 'dateFrom', 'dateTo'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
306
src/Controller/AdminController.php
Normal file
306
src/Controller/AdminController.php
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file AdminController.php
|
||||
* @brief Controleur pour la gestion des utilisateurs par les administrateurs
|
||||
*
|
||||
* Permet aux admins de gerer les comptes utilisateurs : modifier les roles,
|
||||
* activer/desactiver des comptes, changer email et mot de passe,
|
||||
* et assigner des accés aux serveurs.
|
||||
* Toutes les routes de ce controleur necesitent ROLE_ADMIN.
|
||||
*
|
||||
* @package App\Controller
|
||||
* @author Yasser
|
||||
*/
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\ServerAccess;
|
||||
use App\Entity\User;
|
||||
use App\Form\AdminEmailType;
|
||||
use App\Form\AdminPasswordChangeType;
|
||||
use App\Form\UserType;
|
||||
use App\Repository\ServerAccessRepository;
|
||||
use App\Repository\ServerRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
* @class AdminController
|
||||
* @brief Gestion des utilisateurs et de leurs accés aux serveurs
|
||||
*
|
||||
* Reservé aux administrateurs. Permet de:
|
||||
* - Lister tous les utilisateurs
|
||||
* - Modifier les roles, l'etat, l'email et le mot de passe d'un compte
|
||||
* - Assigner/revoquer l'acces a des serveurs pour un operateur
|
||||
* - Activer ou desactiver un compte utilisateur
|
||||
* L'admin peut aussi editer son propre compte mais les roles sont grisés.
|
||||
*/
|
||||
#[Route('/admin')]
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
class AdminController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @brief Affiche la liste de tout les utilisateurs de l'application
|
||||
*
|
||||
* @param UserRepository $userRepo Repository des utilisateurs
|
||||
* @return Response Vue avec la liste triée par date d'inscription (plus recent en premier)
|
||||
*/
|
||||
#[Route('/users', name: 'app_admin_users')]
|
||||
public function users(UserRepository $userRepo): Response
|
||||
{
|
||||
return $this->render('admin/users.html.twig', [
|
||||
'users' => $userRepo->findBy([], ['createdAt' => 'DESC']),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Edition d'un compte utilisateur (roles, statut actif)
|
||||
*
|
||||
* Contrairement a avant, un admin peut maintenant editer son propre compte.
|
||||
* Dans ce cas on passe isSelf=true au template pour griser les roles/statut
|
||||
* et empecher l'admin de se retirer ses propres privileges par accident.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param User $user L'utilisateur a modifier (resolu par Doctrine)
|
||||
* @param EntityManagerInterface $em
|
||||
* @param ServerRepository $serverRepo Pour afficher les serveurs disponibles
|
||||
* @param ServerAccessRepository $accessRepo Pour afficher les acces existants
|
||||
* @return Response Le formulaire d'edition ou redirection
|
||||
*/
|
||||
#[Route('/users/{id}/edit', name: 'app_admin_user_edit', requirements: ['id' => '\d+'])]
|
||||
public function editUser(
|
||||
Request $request,
|
||||
User $user,
|
||||
EntityManagerInterface $em,
|
||||
ServerRepository $serverRepo,
|
||||
ServerAccessRepository $accessRepo,
|
||||
): Response {
|
||||
$isSelf = ($user === $this->getUser());
|
||||
|
||||
// formulaire roles/statut - soumis seulement si pas soi meme
|
||||
$form = $this->createForm(UserType::class, $user);
|
||||
|
||||
// formulaire email - onglet 2
|
||||
$emailForm = $this->createForm(AdminEmailType::class, $user, [
|
||||
'action' => $this->generateUrl('app_admin_user_change_email', ['id' => $user->getId()]),
|
||||
]);
|
||||
|
||||
// formulaire mot de passe - onglet 3
|
||||
$passwordForm = $this->createForm(AdminPasswordChangeType::class, null, [
|
||||
'action' => $this->generateUrl('app_admin_user_change_password', ['id' => $user->getId()]),
|
||||
]);
|
||||
|
||||
if (!$isSelf) {
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$em->flush();
|
||||
$this->addFlash('success', 'Utilisateur mis a jour.');
|
||||
return $this->redirectToRoute('app_admin_user_edit', ['id' => $user->getId()]);
|
||||
}
|
||||
}
|
||||
|
||||
$accesses = $accessRepo->findBy(['user' => $user]);
|
||||
$assignedIds = array_map(fn($a) => $a->getServer()->getId(), $accesses);
|
||||
$allServers = $serverRepo->findBy([], ['name' => 'ASC']);
|
||||
|
||||
return $this->render('admin/edit_user.html.twig', [
|
||||
'form' => $form,
|
||||
'emailForm' => $emailForm,
|
||||
'passwordForm' => $passwordForm,
|
||||
'user' => $user,
|
||||
'accesses' => $accesses,
|
||||
'allServers' => $allServers,
|
||||
'assignedIds' => $assignedIds,
|
||||
'isSelf' => $isSelf,
|
||||
// onglet actif selon la query string pour rester sur le bon apres soumission
|
||||
'activeTab' => $request->query->get('tab', 'roles'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Change l'adresse email d'un utilisateur
|
||||
*
|
||||
* Route dediée pour le formulaire email de l'onglet "Email".
|
||||
* Verifie que le nouvel email n'est pas deja utilisé par un autre compte
|
||||
* avant de sauvegarder. L'admin peut changer son propre email aussi.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param User $user L'utilisateur dont on change l'email
|
||||
* @param EntityManagerInterface $em
|
||||
* @param UserRepository $userRepo Pour verifier l'unicité de l'email
|
||||
* @return Response Redirection vers la page d'edition avec l'onglet email actif
|
||||
*/
|
||||
#[Route('/users/{id}/change-email', name: 'app_admin_user_change_email', methods: ['POST'], requirements: ['id' => '\d+'])]
|
||||
public function changeUserEmail(
|
||||
Request $request,
|
||||
User $user,
|
||||
EntityManagerInterface $em,
|
||||
UserRepository $userRepo,
|
||||
): Response {
|
||||
$emailForm = $this->createForm(AdminEmailType::class, $user);
|
||||
$emailForm->handleRequest($request);
|
||||
|
||||
if ($emailForm->isSubmitted() && $emailForm->isValid()) {
|
||||
$newEmail = $emailForm->get('email')->getData();
|
||||
|
||||
// verifier que cet email est pas deja pris par quelqu'un d'autre
|
||||
$existing = $userRepo->findOneBy(['email' => $newEmail]);
|
||||
if ($existing && $existing->getId() !== $user->getId()) {
|
||||
$this->addFlash('danger', 'Cet email est déjà utilisé par un autre compte.');
|
||||
return $this->redirectToRoute('app_admin_user_edit', ['id' => $user->getId(), 'tab' => 'email']);
|
||||
}
|
||||
|
||||
$em->flush();
|
||||
$this->addFlash('success', 'Email mis a jour.');
|
||||
} else {
|
||||
$this->addFlash('danger', 'Email invalide.');
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('app_admin_user_edit', ['id' => $user->getId(), 'tab' => 'email']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Change le mot de passe d'un utilisateur (privilege admin)
|
||||
*
|
||||
* L'admin n'a pas besoin de connaitre l'ancien mot de passe.
|
||||
* Le nouveau mot de passe est hashé avant d'etre persisté.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param User $user L'utilisateur dont on change le mot de passe
|
||||
* @param EntityManagerInterface $em
|
||||
* @param UserPasswordHasherInterface $passwordHasher
|
||||
* @return Response Redirection vers la page d'edition avec l'onglet password actif
|
||||
*/
|
||||
#[Route('/users/{id}/change-password', name: 'app_admin_user_change_password', methods: ['POST'], requirements: ['id' => '\d+'])]
|
||||
public function changeUserPassword(
|
||||
Request $request,
|
||||
User $user,
|
||||
EntityManagerInterface $em,
|
||||
UserPasswordHasherInterface $passwordHasher,
|
||||
): Response {
|
||||
$passwordForm = $this->createForm(AdminPasswordChangeType::class);
|
||||
$passwordForm->handleRequest($request);
|
||||
|
||||
if ($passwordForm->isSubmitted() && $passwordForm->isValid()) {
|
||||
$newPassword = $passwordForm->get('newPassword')->getData();
|
||||
$user->setPassword($passwordHasher->hashPassword($user, $newPassword));
|
||||
$em->flush();
|
||||
$this->addFlash('success', 'Mot de passe mis a jour pour ' . $user->getPseudo() . '.');
|
||||
} else {
|
||||
$this->addFlash('danger', 'Formulaire invalide, verifie les champs.');
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('app_admin_user_edit', ['id' => $user->getId(), 'tab' => 'password']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Attribue l'accés a un serveur pour un operateur
|
||||
*
|
||||
* Cree un ServerAccess avec les credentials SSH specifique a cet operateur.
|
||||
* Verifie le token CSRF et que l'acces n'existe pas deja pour eviter les doublons.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param User $user L'operateur qui recoit l'acces
|
||||
* @param EntityManagerInterface $em
|
||||
* @param ServerRepository $serverRepo
|
||||
* @param ServerAccessRepository $accessRepo Pour verifier si l'acces existe déja
|
||||
* @return Response Redirection vers la page d'edition de l'utilisateur
|
||||
*/
|
||||
#[Route('/users/{id}/access/grant', name: 'app_admin_grant_access', methods: ['POST'], requirements: ['id' => '\d+'])]
|
||||
public function grantAccess(
|
||||
Request $request,
|
||||
User $user,
|
||||
EntityManagerInterface $em,
|
||||
ServerRepository $serverRepo,
|
||||
ServerAccessRepository $accessRepo,
|
||||
): Response {
|
||||
if (!$this->isCsrfTokenValid('grant_access_' . $user->getId(), $request->request->get('_token'))) {
|
||||
$this->addFlash('error', 'Invalid token.');
|
||||
return $this->redirectToRoute('app_admin_user_edit', ['id' => $user->getId()]);
|
||||
}
|
||||
|
||||
$server = $serverRepo->find($request->request->get('server_id'));
|
||||
if (!$server || $accessRepo->findOneBy(['server' => $server, 'user' => $user])) {
|
||||
$this->addFlash('warning', 'Access already exists or server not found.');
|
||||
return $this->redirectToRoute('app_admin_user_edit', ['id' => $user->getId()]);
|
||||
}
|
||||
|
||||
$access = new ServerAccess();
|
||||
$access->setServer($server)->setUser($user)
|
||||
->setSshUser($request->request->get('ssh_user'))
|
||||
->setSshPassword($request->request->get('ssh_password'));
|
||||
$em->persist($access);
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash('success', $user->getPseudo() . ' can now access ' . $server->getName() . '.');
|
||||
return $this->redirectToRoute('app_admin_user_edit', ['id' => $user->getId()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Revoque l'acces d'un operateur a un serveur
|
||||
*
|
||||
* Supprime l'entité ServerAccess correspondante. Verifie le CSRF
|
||||
* et que l'acces appartient bien a l'utilisateur en parametre.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param int $userId Id de l'utilisateur concerne
|
||||
* @param int $accessId Id du ServerAccess a supprimer
|
||||
* @param EntityManagerInterface $em
|
||||
* @param ServerAccessRepository $accessRepo
|
||||
* @return Response Redirection vers la page d'edition de l'utilisateur
|
||||
*/
|
||||
#[Route('/users/{userId}/access/{accessId}/revoke', name: 'app_admin_revoke_access', methods: ['POST'])]
|
||||
public function revokeAccess(
|
||||
Request $request,
|
||||
int $userId,
|
||||
int $accessId,
|
||||
EntityManagerInterface $em,
|
||||
ServerAccessRepository $accessRepo,
|
||||
): Response {
|
||||
$access = $accessRepo->find($accessId);
|
||||
if ($access && $access->getUser()->getId() === $userId
|
||||
&& $this->isCsrfTokenValid('revoke_access_' . $accessId, $request->request->get('_token'))) {
|
||||
$em->remove($access);
|
||||
$em->flush();
|
||||
$this->addFlash('success', 'Access revoked.');
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('app_admin_user_edit', ['id' => $userId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Active ou desactive un compte utilisateur
|
||||
*
|
||||
* Inverse l'etat isActive de l'utilisateur. Un admin ne peut pas
|
||||
* se desactiver lui meme (protection contre les erreur).
|
||||
* Le token CSRF est verifié avant de modifier l'etat.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param User $user L'utilisateur a activer/desactiver
|
||||
* @param EntityManagerInterface $em
|
||||
* @return Response Redirection vers la liste des utilisateurs
|
||||
*/
|
||||
#[Route('/users/{id}/toggle', name: 'app_admin_user_toggle', methods: ['POST'], requirements: ['id' => '\d+'])]
|
||||
public function toggleUser(Request $request, User $user, EntityManagerInterface $em): Response
|
||||
{
|
||||
if ($user === $this->getUser()) {
|
||||
$this->addFlash('error', 'You cannot deactivate yourself.');
|
||||
return $this->redirectToRoute('app_admin_users');
|
||||
}
|
||||
|
||||
if ($this->isCsrfTokenValid('toggle_user_' . $user->getId(), $request->request->get('_token'))) {
|
||||
$user->setIsActive(!$user->isActive());
|
||||
$em->flush();
|
||||
$this->addFlash('success', 'User status updated.');
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('app_admin_users');
|
||||
}
|
||||
}
|
||||
162
src/Controller/PasswordResetController.php
Normal file
162
src/Controller/PasswordResetController.php
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file PasswordResetController.php
|
||||
* @brief Controleur pour la reinitialisation de mot de passe par email
|
||||
*
|
||||
* Gere le flux complet de reset : demande via email, validation du token
|
||||
* et definition du nouveau mot de passe. Utilise le champ resetToken
|
||||
* deja present sur l'entité User (valide 1 heure).
|
||||
*
|
||||
* @package App\Controller
|
||||
* @author Yasser
|
||||
*/
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Form\ForgotPasswordType;
|
||||
use App\Form\ResetPasswordType;
|
||||
use App\Repository\UserRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
/**
|
||||
* @class PasswordResetController
|
||||
* @brief Gestion du flux de reinitialisation de mot de passe
|
||||
*
|
||||
* Deux etapes:
|
||||
* 1. L'utilisateur saisit son email → on lui envoie un lien avec un token unique
|
||||
* 2. Il clique sur le lien → il saisit son nouveau mot de passe
|
||||
*
|
||||
* Le token expire apres 1 heure. On affiche toujours un message de succes
|
||||
* meme si l'email n'existe pas (pour pas divulguer si un compte existe).
|
||||
*/
|
||||
class PasswordResetController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @brief Etape 1 : formulaire de demande de reinitialisation
|
||||
*
|
||||
* Si l'utilisateur est deja connecté on le redirige.
|
||||
* On genere un token aleatoire, on l'enregistre dans la base avec
|
||||
* une date d'expiration et on envoie l'email avec le lien de reset.
|
||||
* En cas d'email inconnu on affiche quand meme le message de succes
|
||||
* pour pas permettre de deviner les emails enregistrés.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param UserRepository $userRepo
|
||||
* @param EntityManagerInterface $em
|
||||
* @param MailerInterface $mailer
|
||||
* @return Response Le formulaire ou redirection
|
||||
*/
|
||||
#[Route('/forgot-password', name: 'app_forgot_password')]
|
||||
public function forgotPassword(
|
||||
Request $request,
|
||||
UserRepository $userRepo,
|
||||
EntityManagerInterface $em,
|
||||
MailerInterface $mailer,
|
||||
): Response {
|
||||
if ($this->getUser()) {
|
||||
return $this->redirectToRoute('app_home');
|
||||
}
|
||||
|
||||
$form = $this->createForm(ForgotPasswordType::class);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$email = $form->get('email')->getData();
|
||||
$user = $userRepo->findOneBy(['email' => $email]);
|
||||
|
||||
// on envoie l'email uniquement si le compte existe
|
||||
if ($user) {
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$expiresAt = new \DateTimeImmutable('+1 hour');
|
||||
|
||||
$user->setResetToken($token);
|
||||
$user->setResetTokenExpiresAt($expiresAt);
|
||||
$em->flush();
|
||||
|
||||
$resetUrl = $this->generateUrl(
|
||||
'app_reset_password',
|
||||
['token' => $token],
|
||||
UrlGeneratorInterface::ABSOLUTE_URL
|
||||
);
|
||||
|
||||
$message = (new Email())
|
||||
->from('noreply@servercontrol.local')
|
||||
->to($user->getEmail())
|
||||
->subject('ServerControl - Reinitialisation de mot de passe')
|
||||
->html($this->renderView('emails/reset_password.html.twig', [
|
||||
'user' => $user,
|
||||
'resetUrl' => $resetUrl,
|
||||
]));
|
||||
|
||||
$mailer->send($message);
|
||||
}
|
||||
|
||||
// on affiche toujours le meme message pour pas divulguer si l'email existe
|
||||
$this->addFlash('success', 'Si un compte correspond a cet email, vous allez recevoir un lien de reinitialisation.');
|
||||
return $this->redirectToRoute('app_forgot_password');
|
||||
}
|
||||
|
||||
return $this->render('security/forgot_password.html.twig', ['form' => $form]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Etape 2 : validation du token et saisie du nouveau mot de passe
|
||||
*
|
||||
* Verifie que le token existe en base et qu'il n'est pas expiré.
|
||||
* Si valide, affiche le formulaire de nouveau mot de passe.
|
||||
* Apres succes, le token est effacé et l'utilisateur est redirigé vers le login.
|
||||
*
|
||||
* @param string $token Le token de reset depuis l'URL
|
||||
* @param Request $request
|
||||
* @param UserRepository $userRepo
|
||||
* @param EntityManagerInterface $em
|
||||
* @param UserPasswordHasherInterface $passwordHasher
|
||||
* @return Response Le formulaire de reset ou redirection
|
||||
*/
|
||||
#[Route('/reset-password/{token}', name: 'app_reset_password')]
|
||||
public function resetPassword(
|
||||
string $token,
|
||||
Request $request,
|
||||
UserRepository $userRepo,
|
||||
EntityManagerInterface $em,
|
||||
UserPasswordHasherInterface $passwordHasher,
|
||||
): Response {
|
||||
$user = $userRepo->findOneBy(['resetToken' => $token]);
|
||||
|
||||
// token invalide ou expiré
|
||||
if (!$user || !$user->getResetTokenExpiresAt() || $user->getResetTokenExpiresAt() < new \DateTimeImmutable()) {
|
||||
$this->addFlash('danger', 'Ce lien de reinitialisation est invalide ou a expiré.');
|
||||
return $this->redirectToRoute('app_forgot_password');
|
||||
}
|
||||
|
||||
$form = $this->createForm(ResetPasswordType::class);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$newPassword = $form->get('plainPassword')->getData();
|
||||
|
||||
$user->setPassword($passwordHasher->hashPassword($user, $newPassword));
|
||||
// on efface le token apres utilisation pour qu'il ne peut plus etre reutilisé
|
||||
$user->setResetToken(null);
|
||||
$user->setResetTokenExpiresAt(null);
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash('success', 'Votre mot de passe a bien été mis a jour. Vous pouvez vous connecter.');
|
||||
return $this->redirectToRoute('app_login');
|
||||
}
|
||||
|
||||
return $this->render('security/reset_password.html.twig', [
|
||||
'form' => $form,
|
||||
'token' => $token,
|
||||
]);
|
||||
}
|
||||
}
|
||||
80
src/Controller/ProfileController.php
Normal file
80
src/Controller/ProfileController.php
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file ProfileController.php
|
||||
* @brief Controleur pour la gestion du profil de l'utilisateur connecté
|
||||
*
|
||||
* Accessible a tout les utilisateurs connectés (ROLE_OPERATOR minimum).
|
||||
* Permet de changer son propre mot de passe en fournisant l'ancien.
|
||||
*
|
||||
* @package App\Controller
|
||||
* @author Yasser
|
||||
*/
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Form\ChangePasswordType;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
* @class ProfileController
|
||||
* @brief Gestion du profil et du mot de passe pour l'utilisateur connecté
|
||||
*
|
||||
* Tout les opérateurs et admins peuvent acceder a cette page pour changer
|
||||
* leur propre mot de passe. L'admin a aussi acces via la page admin/users
|
||||
* pour changer le mot de passe des autres.
|
||||
*/
|
||||
#[IsGranted('ROLE_OPERATOR')]
|
||||
class ProfileController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @brief Page de profil avec le formulaire de changement de mot de passe
|
||||
*
|
||||
* Verifie que l'ancien mot de passe saisi correspond bien au hash stocké
|
||||
* en base avant d'autoriser le changement. Si incorrect, on affiche
|
||||
* une erreur sans reveler d'informations supplementaires.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param UserPasswordHasherInterface $passwordHasher Pour verifier et hasher le mot de passe
|
||||
* @param EntityManagerInterface $em
|
||||
* @return Response La page de profil avec le formulaire
|
||||
*/
|
||||
#[Route('/profile', name: 'app_profile')]
|
||||
public function profile(
|
||||
Request $request,
|
||||
UserPasswordHasherInterface $passwordHasher,
|
||||
EntityManagerInterface $em,
|
||||
): Response {
|
||||
/** @var \App\Entity\User $user */
|
||||
$user = $this->getUser();
|
||||
$form = $this->createForm(ChangePasswordType::class);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$currentPassword = $form->get('currentPassword')->getData();
|
||||
$newPassword = $form->get('newPassword')->getData();
|
||||
|
||||
// verifier que l'ancien mot de passe est correcte avant de changer
|
||||
if (!$passwordHasher->isPasswordValid($user, $currentPassword)) {
|
||||
$this->addFlash('danger', 'Mot de passe actuel incorrect.');
|
||||
return $this->redirectToRoute('app_profile');
|
||||
}
|
||||
|
||||
$user->setPassword($passwordHasher->hashPassword($user, $newPassword));
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash('success', 'Votre mot de passe a bien été mis a jour.');
|
||||
return $this->redirectToRoute('app_profile');
|
||||
}
|
||||
|
||||
return $this->render('security/profile.html.twig', [
|
||||
'form' => $form,
|
||||
]);
|
||||
}
|
||||
}
|
||||
147
src/Controller/SecurityController.php
Normal file
147
src/Controller/SecurityController.php
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file SecurityController.php
|
||||
* @brief Controleur gérant l'authentification et l'inscription des utilisateurs
|
||||
*
|
||||
* Prend en charge la page de connexion, deconnexion, inscription et
|
||||
* la page d'attente pour les comptes non encore validé par un admin.
|
||||
*
|
||||
* @package App\Controller
|
||||
* @author Yasser
|
||||
*/
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Form\RegistrationFormType;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||
|
||||
/**
|
||||
* @class SecurityController
|
||||
* @brief Gestion de l'authentification et des comptes utilisateurs
|
||||
*
|
||||
* Ce controleur gere toute la partie authentification de l'appli.
|
||||
* Les nouveaux comptes sont crée avec ROLE_USER et doivent etre
|
||||
* promu par un admin avant de pouvoir acceder aux serveurs.
|
||||
*/
|
||||
class SecurityController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @brief Page d'accueil - redirige selon le statut de connexion
|
||||
*
|
||||
* Si connecté et operateur/admin : redirige vers la liste des serveurs.
|
||||
* Si connecté mais pas encore validé : redirige vers la page d'attente.
|
||||
* Si pas connecté : redirige vers le login.
|
||||
*
|
||||
* @return Response Redirection vers la bonne page
|
||||
*/
|
||||
#[Route('/', name: 'app_home')]
|
||||
public function home(): Response
|
||||
{
|
||||
if ($this->getUser()) {
|
||||
return $this->isGranted('ROLE_OPERATOR')
|
||||
? $this->redirectToRoute('app_server_index')
|
||||
: $this->redirectToRoute('app_pending');
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('app_login');
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Page d'attente pour les comptes non encore promu par un admin
|
||||
*
|
||||
* Affiché aux utilisateurs avec ROLE_USER uniquement.
|
||||
* Si l'utilisateur est déja opérateur il est redirigé automatiquement.
|
||||
*
|
||||
* @return Response La page d'attente ou une redirection
|
||||
*/
|
||||
#[Route('/pending', name: 'app_pending')]
|
||||
public function pending(): Response
|
||||
{
|
||||
if ($this->isGranted('ROLE_OPERATOR')) {
|
||||
return $this->redirectToRoute('app_server_index');
|
||||
}
|
||||
|
||||
return $this->render('security/pending.html.twig');
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Page de connexion
|
||||
*
|
||||
* Affiche le formulaire de login. Si l'utilisateur est déja connecté
|
||||
* on le redirige directement vers la home page.
|
||||
* Symfony gere la soumission du form via le firewall, pas ici.
|
||||
*
|
||||
* @param AuthenticationUtils $authenticationUtils Utilitaire Symfony pour recuperer les erreurs
|
||||
* @return Response La page de login avec eventuellement un message d'erreur
|
||||
*/
|
||||
#[Route('/login', name: 'app_login')]
|
||||
public function login(AuthenticationUtils $authenticationUtils): Response
|
||||
{
|
||||
if ($this->getUser()) {
|
||||
return $this->redirectToRoute('app_home');
|
||||
}
|
||||
|
||||
return $this->render('security/login.html.twig', [
|
||||
'last_username' => $authenticationUtils->getLastUsername(),
|
||||
'error' => $authenticationUtils->getLastAuthenticationError(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Route de deconnexion - géré automatiquement par Symfony
|
||||
*
|
||||
* Cette methode est vide car Symfony intercepte la route /logout
|
||||
* via la configuration du firewall avant que le code soit executé.
|
||||
*/
|
||||
#[Route('/logout', name: 'app_logout')]
|
||||
public function logout(): void {}
|
||||
|
||||
/**
|
||||
* @brief Page et traitement du formulaire d'inscription
|
||||
*
|
||||
* Cree un nouveau compte avec ROLE_USER par defaut. Le mot de passe
|
||||
* est hashé avec le PasswordHasher de Symfony avant d'etre persisté.
|
||||
* Le compte devra etre validé par un admin pour avoir acces aux serveurs.
|
||||
*
|
||||
* @param Request $request La requete HTTP
|
||||
* @param UserPasswordHasherInterface $passwordHasher Pour hasher le mot de passe
|
||||
* @param EntityManagerInterface $em Pour persister l'utilisateur en base
|
||||
* @return Response Le formulaire d'inscription ou une redirection apres succes
|
||||
*/
|
||||
#[Route('/register', name: 'app_register')]
|
||||
public function register(
|
||||
Request $request,
|
||||
UserPasswordHasherInterface $passwordHasher,
|
||||
EntityManagerInterface $em,
|
||||
): Response {
|
||||
if ($this->getUser()) {
|
||||
return $this->redirectToRoute('app_home');
|
||||
}
|
||||
|
||||
$user = new User();
|
||||
$form = $this->createForm(RegistrationFormType::class, $user);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$user->setPassword(
|
||||
$passwordHasher->hashPassword($user, $form->get('plainPassword')->getData())
|
||||
);
|
||||
$user->setRoles(['ROLE_USER']);
|
||||
$em->persist($user);
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash('success', 'Account created! You can now log in.');
|
||||
return $this->redirectToRoute('app_login');
|
||||
}
|
||||
|
||||
return $this->render('security/register.html.twig', ['form' => $form]);
|
||||
}
|
||||
}
|
||||
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