src/Security/PropietarioContenidoAuthorizationService.php line 38

Open in your IDE?
  1. <?php
  2. namespace App\Security;
  3. use App\Entity\ConsorciPropietarioContenido;
  4. use App\Entity\PropietarioContenido;
  5. use App\Entity\PropietarioContenidoHasUsuarioHermes;
  6. use App\Entity\UsuarioHermes;
  7. use App\Enum\RolEnum;
  8. use App\Interfaces\UsuarioHermesRepositoryInterface;
  9. use App\Services\AsymmetricEncryptionService;
  10. use Symfony\Component\HttpFoundation\RedirectResponse;
  11. use Symfony\Component\HttpFoundation\RequestStack;
  12. use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
  13. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  14. use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
  15. use Symfony\Component\Security\Core\Security;
  16. use Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken;
  17. /**
  18. * Servicio para controlar el acceso a recursos protegidos basado en la relación
  19. * entre UsuarioHermes y PropietarioContenido.
  20. */
  21. class PropietarioContenidoAuthorizationService
  22. {
  23. private FlashBagInterface $flashBag;
  24. public function __construct(
  25. private TokenStorageInterface $tokenStorage,
  26. private Security $security,
  27. private UrlGeneratorInterface $urlGenerator,
  28. private RequestStack $requestStack,
  29. private AsymmetricEncryptionService $asymmetricEncryptionService,
  30. private UsuarioHermesRepositoryInterface $usuarioHermesRepository,
  31. ) {
  32. $this->flashBag = $this->requestStack->getSession()->getFlashBag();
  33. }
  34. /**
  35. * Verifica si el usuario tiene acceso a recursos asociados a los tipos de PropietarioContenido especificados.
  36. *
  37. * @param array $rolesPermitidos Array de roles permitidos de RolEnum (ej. [RolEnum::PARTICIPANTE, RolEnum::FORMADOR])
  38. * @param string|null $rolEsperado Rol esperado para este recurso (opcional)
  39. * @param bool $redirectOnFailure Si debe redirigir al usuario en caso de fallo de autorización
  40. * @param bool $throwException Si debe lanzar excepciones personalizadas en lugar de devolver false
  41. * @param string $severityLevel Nivel de severidad para los mensajes de error (info, warning, error, critical)
  42. * @return bool|RedirectResponse True si autorizado, RedirectResponse si no autorizado y redirectOnFailure=true
  43. * @throws \App\Exception\Authorization\PropietarioContenidoAuthorizationException Si el acceso es denegado y $throwException=true
  44. */
  45. public function verificarAcceso(
  46. array $rolesPermitidos,
  47. ?string $rolEsperado = null,
  48. bool $redirectOnFailure = true,
  49. bool $throwException = false,
  50. string $severityLevel = \App\Exception\Authorization\PropietarioContenidoAuthorizationException::SEVERITY_ERROR
  51. ) {
  52. // --- INICIO: Lógica de Autologin ---
  53. $request = $this->requestStack->getCurrentRequest();
  54. if ($request && $tokenParam = $request->query->get('autologinToken')) {
  55. try {
  56. $decryptedPayload = $this->asymmetricEncryptionService->hybridDecrypt($tokenParam);
  57. // Validar expiración y payload
  58. if (is_array($decryptedPayload) && isset($decryptedPayload['userId'], $decryptedPayload['expires']) && $decryptedPayload['expires'] > time()) {
  59. $user = $this->usuarioHermesRepository->findById($decryptedPayload['userId']);
  60. if ($user) {
  61. // Seleccionar automáticamente el primer PropietarioContenido activo
  62. $propietarioSeleccionado = $this->seleccionarPropietarioContenidoActivo($user);
  63. // Crear y almacenar el token de autenticación
  64. $authToken = new PostAuthenticationToken($user, 'main', $user->getRoles() ?? []);
  65. $this->tokenStorage->setToken($authToken);
  66. // Guardar en sesión
  67. $session = $this->requestStack->getSession();
  68. $session->set('_security_main', serialize($authToken));
  69. // Guardar PropietarioContenido y rol activo en la sesión si se seleccionó uno
  70. if ($propietarioSeleccionado) {
  71. $rolSeleccionado = RolEnum::getRolFromClass(get_class($propietarioSeleccionado));
  72. if ($rolSeleccionado) {
  73. $session->set('rol', $rolSeleccionado);
  74. }
  75. }
  76. $session->save();
  77. // Redirigir a la misma URL pero sin el token para limpiarla
  78. $queryParams = $request->query->all();
  79. unset($queryParams['autologinToken']);
  80. $redirectUrl = $this->urlGenerator->generate($request->attributes->get('_route'), array_merge($request->attributes->get('_route_params'), $queryParams));
  81. return new RedirectResponse($redirectUrl);
  82. }
  83. }
  84. } catch (\Exception $e) {
  85. // Token inválido o expirado. Mostrar mensaje flash y continuar flujo normal.
  86. $this->flashBag->add('error', 'El enlace de acceso automático ha expirado o no es válido. Por favor, inicie sesión normalmente.');
  87. }
  88. }
  89. // --- FIN: Lógica de Autologin ---
  90. // Verificar si hay usuario logueado
  91. $token = $this->tokenStorage->getToken();
  92. if (!$token || !$this->security->isGranted('IS_AUTHENTICATED_FULLY')) {
  93. if ($throwException) {
  94. throw new \App\Exception\Authorization\NoUserException(
  95. 'Debe iniciar sesión para acceder a este recurso.',
  96. $severityLevel
  97. );
  98. }
  99. if ($redirectOnFailure) {
  100. $this->flashBag->add('error', 'Debe iniciar sesión para acceder a este recurso.');
  101. return new RedirectResponse($this->urlGenerator->generate('login_request_credentials'));
  102. }
  103. return false;
  104. }
  105. /** @var UsuarioHermes $usuario */
  106. $usuario = $token->getUser();
  107. // Obtener relaciones del usuario con PropietarioContenido
  108. $propietarioRelaciones = $usuario->getPropietarioContenidoHasUsuarioHermes();
  109. if ($propietarioRelaciones->isEmpty()) {
  110. if ($throwException) {
  111. throw new \App\Exception\Authorization\NoRelationsException(
  112. 'Su cuenta no está asociada a ninguna entidad.',
  113. $severityLevel
  114. );
  115. }
  116. if ($redirectOnFailure) {
  117. $this->flashBag->add('error', 'Su cuenta no está asociada a ninguna entidad.');
  118. return new RedirectResponse($this->urlGenerator->generate('home'));
  119. }
  120. return false;
  121. }
  122. // Buscar si hay al menos un PropietarioContenido del tipo permitido
  123. $coincidenciaEncontrada = false;
  124. $relacionSinValidar = false;
  125. foreach ($propietarioRelaciones as $relacion) {
  126. $propietarioContenido = $relacion->getPropietarioContenido();
  127. $rolPropietario = RolEnum::getRolFromClass(get_class($propietarioContenido));
  128. if ($rolPropietario && in_array($rolPropietario, $rolesPermitidos)) {
  129. // Verificar si la relación está validada
  130. if ($relacion->getValidatedAt() === null) {
  131. $relacionSinValidar = true;
  132. continue;
  133. }
  134. // Si se requiere un rol específico, verificar que el usuario lo tenga
  135. if ($rolEsperado !== null && !$this->security->isGranted($rolEsperado)) {
  136. if ($throwException) {
  137. throw new \App\Exception\Authorization\IncorrectRoleException(
  138. $rolEsperado,
  139. 'Para acceder a este recurso necesita cambiar a un rol diferente.',
  140. $severityLevel
  141. );
  142. }
  143. if ($redirectOnFailure) {
  144. $this->flashBag->add('warning', 'Para acceder a este recurso necesita cambiar a un rol diferente.');
  145. return new RedirectResponse($this->urlGenerator->generate('home'));
  146. }
  147. return false;
  148. }
  149. $coincidenciaEncontrada = true;
  150. // FIX-INC-733-H3: setea propietario_contenido_id_activo cuando session NULL
  151. // o el id activo no corresponde al tipo del rol del propietario validado.
  152. $session = $this->requestStack->getSession();
  153. $propietarioActivoId = $session->get('propietario_contenido_id_activo');
  154. $needsUpdate = false;
  155. if ($propietarioActivoId === null) {
  156. $needsUpdate = true;
  157. } else {
  158. // Comprobar si el id activo en sesion corresponde al tipo de rol validado
  159. $matchTipo = false;
  160. foreach ($propietarioRelaciones as $rel2) {
  161. $pc2 = $rel2->getPropietarioContenido();
  162. if ($pc2->getId() === $propietarioActivoId
  163. && RolEnum::getRolFromClass(get_class($pc2)) === $rolPropietario) {
  164. $matchTipo = true;
  165. break;
  166. }
  167. }
  168. if (!$matchTipo) {
  169. $needsUpdate = true;
  170. }
  171. }
  172. if ($needsUpdate) {
  173. $session->set('propietario_contenido_id_activo', $propietarioContenido->getId());
  174. }
  175. break;
  176. }
  177. }
  178. // Si hay una relación sin validar pero ninguna validada
  179. if (!$coincidenciaEncontrada && $relacionSinValidar) {
  180. if ($throwException) {
  181. throw new \App\Exception\Authorization\RelationNotValidatedException(
  182. $rolesPermitidos,
  183. 'Su cuenta aún no ha sido activada. Por favor, contacte con el administrador.',
  184. $severityLevel
  185. );
  186. }
  187. if ($redirectOnFailure) {
  188. $this->flashBag->add('warning', 'Su cuenta aún no ha sido activada. Por favor, contacte con el administrador.');
  189. return new RedirectResponse($this->urlGenerator->generate('home'));
  190. }
  191. return false;
  192. }
  193. // Si no se encontró ninguna coincidencia
  194. if (!$coincidenciaEncontrada) {
  195. if ($throwException) {
  196. throw new \App\Exception\Authorization\NoMatchingRolException(
  197. $rolesPermitidos,
  198. 'No tiene permisos para acceder al recurso solicitado.',
  199. $severityLevel
  200. );
  201. }
  202. if ($redirectOnFailure) {
  203. $this->flashBag->add('error', 'No tiene permisos para acceder al recurso solicitado.');
  204. return new RedirectResponse($this->urlGenerator->generate('home'));
  205. }
  206. return false;
  207. }
  208. return true;
  209. }
  210. public function canManageEmployee(UsuarioHermes $usuarioActual, PropietarioContenidoHasUsuarioHermes $relacion, ?int $selectedEntidadId = null)
  211. {
  212. // Verificar si es usuario privilegiado y tiene selectedEntidadId
  213. if ($this->isConsorciPrivilegiado($usuarioActual) && $selectedEntidadId) {
  214. return $this->canManageEmployeeForSelectedEntity($usuarioActual, $relacion, $selectedEntidadId);
  215. }
  216. // Lógica original para usuarios no privilegiados
  217. $typePropietarioContenido = get_class($relacion->getPropietarioContenido());
  218. if ($usuarioActual->getPropietarioContenidos()->filter(fn($pc)=>in_array(get_class($pc), [$typePropietarioContenido, ConsorciPropietarioContenido::class]))){
  219. return true;
  220. }
  221. return false;
  222. }
  223. public function canViewEmployee(UsuarioHermes $usuarioActual, PropietarioContenidoHasUsuarioHermes $relacion, ?int $selectedEntidadId = null)
  224. {
  225. return $this->canManageEmployee($usuarioActual, $relacion, $selectedEntidadId);
  226. }
  227. public function canCreateEmployee(UsuarioHermes $usuarioActual, ?\App\Entity\PropietarioContenido $propietarioContenido = null, ?int $selectedEntidadId = null)
  228. {
  229. // Para usuarios privilegiados, verificar que tengan permisos sobre la entidad seleccionada
  230. if ($this->isConsorciPrivilegiado($usuarioActual) && $selectedEntidadId) {
  231. return $this->canCreateEmployeeForSelectedEntity($usuarioActual, $selectedEntidadId);
  232. }
  233. return true;
  234. }
  235. public function canDeleteEmployee(UsuarioHermes $usuarioActual, PropietarioContenido $propietarioContenido, ?int $selectedEntidadId = null): bool
  236. {
  237. // Para usuarios privilegiados, verificar que el empleado pertenezca a la entidad seleccionada
  238. if ($this->isConsorciPrivilegiado($usuarioActual) && $selectedEntidadId) {
  239. return $this->canDeleteEmployeeForSelectedEntity($usuarioActual, $propietarioContenido, $selectedEntidadId);
  240. }
  241. return true;
  242. }
  243. /**
  244. * Verifica si el usuario autenticado es un "Usuario Consorci Privilegiado".
  245. * Un usuario es privilegiado si tiene asociada al menos una entidad de tipo ConsorciPropietarioContenido.
  246. *
  247. * @param UsuarioHermes|null $usuario Usuario a verificar. Si es null, usa el usuario actual.
  248. * @return bool True si es usuario privilegiado, false en caso contrario
  249. */
  250. public function isConsorciPrivilegiado(?UsuarioHermes $usuario = null): bool
  251. {
  252. if ($usuario === null) {
  253. $token = $this->tokenStorage->getToken();
  254. if (!$token || !($token->getUser() instanceof UsuarioHermes)) {
  255. return false;
  256. }
  257. $usuario = $token->getUser();
  258. }
  259. $propietarioRelaciones = $usuario->getPropietarioContenidoHasUsuarioHermes();
  260. foreach ($propietarioRelaciones as $relacion) {
  261. $propietarioContenido = $relacion->getPropietarioContenido();
  262. if ($propietarioContenido instanceof ConsorciPropietarioContenido) {
  263. return true;
  264. }
  265. }
  266. return false;
  267. }
  268. /**
  269. * Obtiene la entidad Consorci asociada al usuario privilegiado.
  270. *
  271. * @param UsuarioHermes|null $usuario Usuario a verificar. Si es null, usa el usuario actual.
  272. * @return \App\Entity\Consorci|null La entidad Consorci asociada o null si no se encuentra
  273. */
  274. public function getConsorciAssociated(?UsuarioHermes $usuario = null): ?\App\Entity\Consorci
  275. {
  276. if ($usuario === null) {
  277. $token = $this->tokenStorage->getToken();
  278. if (!$token || !($token->getUser() instanceof UsuarioHermes)) {
  279. return null;
  280. }
  281. $usuario = $token->getUser();
  282. }
  283. $propietarioRelaciones = $usuario->getPropietarioContenidoHasUsuarioHermes();
  284. foreach ($propietarioRelaciones as $relacion) {
  285. $propietarioContenido = $relacion->getPropietarioContenido();
  286. if ($propietarioContenido instanceof ConsorciPropietarioContenido) {
  287. return $propietarioContenido->getEntidadAsociada();
  288. }
  289. }
  290. return null;
  291. }
  292. /**
  293. * Verifica si un usuario privilegiado puede gestionar un empleado específico para una entidad seleccionada.
  294. */
  295. private function canManageEmployeeForSelectedEntity(UsuarioHermes $usuarioActual, PropietarioContenidoHasUsuarioHermes $relacion, int $selectedEntidadId): bool
  296. {
  297. $propietarioContenido = $relacion->getPropietarioContenido();
  298. $entidadAsociada = $propietarioContenido->getEntidadAsociada();
  299. return $entidadAsociada && $entidadAsociada->getId() === $selectedEntidadId;
  300. }
  301. /**
  302. * Verifica si un usuario privilegiado puede crear empleados para una entidad seleccionada.
  303. */
  304. private function canCreateEmployeeForSelectedEntity(UsuarioHermes $usuarioActual, int $selectedEntidadId): bool
  305. {
  306. // Los usuarios privilegiados pueden crear empleados para cualquier entidad
  307. // La validación adicional se puede implementar aquí si es necesario
  308. return true;
  309. }
  310. /**
  311. * Verifica si un usuario privilegiado puede eliminar un empleado específico para una entidad seleccionada.
  312. */
  313. private function canDeleteEmployeeForSelectedEntity(UsuarioHermes $usuarioActual, PropietarioContenido $propietarioContenido, int $selectedEntidadId): bool
  314. {
  315. $entidadAsociada = $propietarioContenido->getEntidadAsociada();
  316. return $entidadAsociada && $entidadAsociada->getId() === $selectedEntidadId;
  317. }
  318. public function canEditConvocatoria(?\Symfony\Component\Security\Core\User\UserInterface $user, mixed $convocatoria)
  319. {
  320. return true;
  321. }
  322. /**
  323. * Verifica si el propietario_contenido es organizador del grupo dado.
  324. *
  325. * Trazabilidad:
  326. * - HU-389v2-02 CA-4 (forbidden no-organizador)
  327. * - ADR-003 autorizacion endpoint pending-members
  328. *
  329. * Logica: el propietario es organizador si tiene rol CONSORCI (gestiona todos
  330. * los grupos), ENTIDAD_COLABORATIVA con el grupo en sus expedientes, o FORMADOR
  331. * con el grupo entre los que imparte. La resolucion fina queda como TODO; la
  332. * firma publica + la clase ya permiten que los tests mocken este servicio.
  333. *
  334. * @param int $grupoId
  335. * @param int $propietarioContenidoId
  336. * @return bool
  337. */
  338. public function isOrganizadorDelGrupo(int $grupoId, int $propietarioContenidoId): bool
  339. {
  340. // INC-389v2 demo MVP: CONSORCI siempre es organizador (rol global).
  341. // Resolución vía Security del usuario actual (sin EntityManager directo).
  342. $user = $this->security->getUser();
  343. if (!$user instanceof UsuarioHermes) {
  344. return false;
  345. }
  346. foreach ($user->getPropietarioContenidoHasUsuarioHermes() as $rel) {
  347. $pc = $rel->getPropietarioContenido();
  348. if ($pc instanceof \App\Entity\ConsorciPropietarioContenido) {
  349. return true;
  350. }
  351. }
  352. // TODO HU-389v2-followup: FORMADOR (grupos que imparte), ENTIDAD_COLABORATIVA (grupos que organiza).
  353. return false;
  354. }
  355. /**
  356. * Selecciona automáticamente el primer PropietarioContenido activo del usuario.
  357. * Prioriza los que sean rol PARTICIPANTE si los hay.
  358. *
  359. * @param UsuarioHermes $user Usuario para el cual seleccionar el PropietarioContenido
  360. * @return PropietarioContenido|null El PropietarioContenido seleccionado o null si no se encontró
  361. */
  362. private function seleccionarPropietarioContenidoActivo(UsuarioHermes $user): ?PropietarioContenido
  363. {
  364. $propietarioRelaciones = $user->getPropietarioContenidoHasUsuarioHermes();
  365. if ($propietarioRelaciones->isEmpty()) {
  366. return null;
  367. }
  368. $propietariosActivos = [];
  369. $propietariosParticipante = [];
  370. // Buscar PropietarioContenido activos (validados)
  371. foreach ($propietarioRelaciones as $relacion) {
  372. // Solo considerar relaciones validadas
  373. if ($relacion->getValidatedAt() === null) {
  374. continue;
  375. }
  376. $propietarioContenido = $relacion->getPropietarioContenido();
  377. $rolPropietario = RolEnum::getRolFromClass(get_class($propietarioContenido));
  378. if ($rolPropietario) {
  379. $propietariosActivos[] = $propietarioContenido;
  380. // Separar los de rol PARTICIPANTE para priorizarlos
  381. if ($rolPropietario === RolEnum::PARTICIPANTE) {
  382. $propietariosParticipante[] = $propietarioContenido;
  383. }
  384. }
  385. }
  386. // Si no hay PropietarioContenido activos, no hacer nada
  387. if (empty($propietariosActivos)) {
  388. return null;
  389. }
  390. // Priorizar rol PARTICIPANTE si existe, sino tomar el primero activo
  391. $propietarioSeleccionado = !empty($propietariosParticipante)
  392. ? $propietariosParticipante[0]
  393. : $propietariosActivos[0];
  394. return $propietarioSeleccionado;
  395. }
  396. }