← Proyecto backend completo

Sesión 49 · Semana 25

Completar núcleo, seguridad e integración

Proyecto compartido. En el taller de Intermodular que abre esta semana has trabajado ensayar la recuperación y publicar el incremento. En Servidor continúas la implementación del mismo producto.

Se explica

25 minutos · explicación y demostración

El primer recorrido de la ampliación funciona. Hoy completarás sus reglas, transacciones y permisos. Una máquina de estados describe estados permitidos y transiciones; resulta útil cuando una operación, como cerrar o aprobar, depende de la situación actual del recurso.

La trampa de la dispersión frente al núcleo funcional

Cuando un desarrollador afronta un proyecto grande, la tentación habitual es crear quince entidades y diez controladores a la vez: la entidad de etiquetas, la de comentarios, la de historial, la de categorías…

  • Al final de la jornada tiene miles de líneas de código escritas, pero ningún caso de uso funciona de verdad.
  • No puede hacer una demo a su cliente ni pasar un test de integración real.

El desarrollo profesional se rige por la priorización por valor:

  • Identifica los dos agregados centrales que dan sentido al negocio (en nuestro caso: Proyecto y Tarea).
  • Implementa sus relaciones y reglas más complejas antes de añadir adornos secundarios.

La ley del núcleo de negocio

Un proyecto sin entidades secundarias es un producto viable; un proyecto con diez entidades a medias es chatarra.

Construye y prueba a fondo las reglas más críticas del dominio (presupuestos, transiciones de estado e integridad referencial) antes de dedicar tiempo a comentarios, avatares o filtros decorativos.

Reglas de negocio e integridad entre Agregados

En nuestro dominio empresarial, un Proyecto actúa como raíz de agregado (Aggregate Root) sobre sus Tareas:

El ciclo de vida transaccional Proyecto-Tarea
  1. 1. Proyecto creado con presupuesto total de 50.000 €
  2. 2. Alta de Tarea A (coste 20.000 €) -> Aceptada (acumulado 20.000 €)
  3. 3. Alta de Tarea B (coste 25.000 €) -> Aceptada (acumulado 45.000 €)
  4. 4. Alta de Tarea C (coste 10.000 €) -> Rechazada (45.000 + 10.000 > 50.000)
  5. 5. Cierre de Proyecto -> Solo permitido si Tareas A y B están FINALIZADAS

Más allá de los roles: Autorización basada en la propiedad del dato

Comprobar roles (ADMINISTRADOR, JEFE_PROYECTO, DESARROLLADOR) es solo la primera línea de defensa.

  • Si el usuario Elena es JEFE_PROYECTO y el usuario Marcos también es JEFE_PROYECTO, Elena no debe poder modificar el presupuesto ni reasignar tareas del proyecto que gestiona Marcos.
  • Esto se conoce como Control de Acceso Basado en Atributos (ABAC) o Seguridad a Nivel de Dominio.
Las dos capas de autorización en Spring Security
  1. 1. Petición HTTP con Bearer JWT
  2. 2. Capa 1: ¿Tiene el Rol adecuado? (RBAC: hasRole)
  3. 3. Capa 2: ¿Es el Propietario del Recurso? (ABAC: esResponsable)
  4. 4. Ejecución del método de negocio

Se trabaja

140 minutos · implementación guiada sobre el proyecto propio

Paso 1 · Retomar el proyecto y preparar la comprobación

  1. Abre los criterios pendientes, el servicio ampliado y la matriz de permisos. Dibuja los estados y cambios válidos del recurso si tu operación los necesita.
  2. Prepara datos para una transición válida, una prohibida y un intento de otro usuario. Anota el estado que debe conservar cada rechazo.
  3. Localiza las llamadas externas dentro del nuevo recorrido y reutiliza los límites de espera y tratamiento de fallos ya definidos.

Paso 2 · Implementación del núcleo transaccional

El controlador mostrado al final también llama a listarTareasProyecto. Añade este método al mismo TareaService, importando Page y Pageable de Spring Data. Mantén los cuatro componentes de TareaResponse que utiliza esta fase del proyecto:

@Transactional(readOnly = true)
public Page<TareaResponse> listarTareasProyecto(Long proyectoId, Pageable pageable) {
    if (!proyectoRepository.existsById(proyectoId)) {
        throw new org.springframework.web.server.ResponseStatusException(
            org.springframework.http.HttpStatus.NOT_FOUND, "Proyecto no encontrado");
    }
    return tareaRepository.findByProyectoId(proyectoId, pageable)
        .map(tarea -> new TareaResponse(tarea.getId(), tarea.getTitulo(),
            tarea.getEstado().name(), tarea.getCosteEstimado()));
}

Conserva los métodos de listado y DTO que tu producto necesite; si amplías un DTO anterior, añade sus componentes también al mapper en lugar de eliminarlos.

Reutiliza el recurso ampliado en la sesión 48. Antes de añadir la consulta de suma, comprueba que presupuesto y coste están definidos como BigDecimal en entidad, DTO y base de datos. Implementa primero la consulta, después la regla en el servicio y por último el endpoint. El bloque de controlador es parcial: conserva o implementa el método de listado al que llama, con su Page y Pageable. Para cada rechazo consulta el estado posterior y verifica que no se ha consumido presupuesto ni creado una tarea.

package com.ejemplo.gestor.tarea.repository;

import com.ejemplo.gestor.tarea.model.EstadoTarea;
import com.ejemplo.gestor.tarea.model.Tarea;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.math.BigDecimal;

public interface TareaRepository extends JpaRepository<Tarea, Long> {

    // Paginación eficiente de tareas por proyecto
    Page<Tarea> findByProyectoId(Long proyectoId, Pageable pageable);

    // Sumatorio de costes en base de datos (rendimiento óptimo en SQL)
    @Query("SELECT COALESCE(SUM(t.costeEstimado), 0.0) FROM Tarea t WHERE t.proyecto.id = :proyectoId")
    BigDecimal calcularCosteTotalEstimadoProyecto(@Param("proyectoId") Long proyectoId);

    // Conteo de tareas no finalizadas para validar el cierre
    long countByProyectoIdAndEstadoNot(Long proyectoId, EstadoTarea estado);
}
package com.ejemplo.gestor.tarea.service;

import com.ejemplo.gestor.proyecto.model.Proyecto;
import com.ejemplo.gestor.proyecto.repository.ProyectoRepository;
import com.ejemplo.gestor.tarea.dto.CrearTareaRequest;
import com.ejemplo.gestor.tarea.dto.TareaResponse;
import com.ejemplo.gestor.tarea.model.EstadoTarea;
import com.ejemplo.gestor.tarea.model.Tarea;
import com.ejemplo.gestor.tarea.repository.TareaRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;

@Service
public class TareaService {

    private final TareaRepository tareaRepository;
    private final ProyectoRepository proyectoRepository;

    public TareaService(TareaRepository tareaRepository, ProyectoRepository proyectoRepository) {
        this.tareaRepository = tareaRepository;
        this.proyectoRepository = proyectoRepository;
    }

    @Transactional
    public TareaResponse crearTarea(Long proyectoId, CrearTareaRequest request) {
        Proyecto proyecto = proyectoRepository.findById(proyectoId)
            .orElseThrow(() -> new IllegalArgumentException("Proyecto no encontrado"));

        // Regla 1: No se pueden añadir tareas a proyectos cerrados o cancelados
        if (proyecto.getEstado().esTerminal()) {
            throw new IllegalStateException("No se pueden añadir tareas a un proyecto en estado " + proyecto.getEstado());
        }

        // Regla 2: El sumatorio de costes no puede superar el presupuesto total del proyecto
        BigDecimal costeActual = tareaRepository.calcularCosteTotalEstimadoProyecto(proyectoId);
        BigDecimal nuevoTotal = costeActual.add(request.costeEstimado());

        if (nuevoTotal.compareTo(proyecto.getPresupuestoTotal()) > 0) {
            throw new IllegalArgumentException(String.format(
                "Presupuesto excedido. Presupuesto proyecto: %.2f €, Coste actual: %.2f €, Nueva tarea: %.2f €",
                proyecto.getPresupuestoTotal(), costeActual, request.costeEstimado()
            ));
        }

        Tarea tarea = new Tarea();
        tarea.setTitulo(request.titulo());
        tarea.setPrioridad(request.prioridad());
        tarea.setCosteEstimado(request.costeEstimado());
        tarea.setProyecto(proyecto);
        tarea.setEstado(EstadoTarea.PENDIENTE);

        tarea = tareaRepository.save(tarea);
        return new TareaResponse(tarea.getId(), tarea.getTitulo(), tarea.getEstado().name(), tarea.getCosteEstimado());
    }
}
    @Transactional
    public void cerrarProyecto(Long proyectoId) {
        Proyecto proyecto = proyectoRepository.findById(proyectoId)
            .orElseThrow(() -> new IllegalArgumentException("Proyecto no encontrado"));

        // Verificamos si existen tareas pendientes de finalizar
        long tareasPendientes = tareaRepository.countByProyectoIdAndEstadoNot(proyectoId, EstadoTarea.FINALIZADA);

        if (tareasPendientes > 0) {
            // Lanzamos excepción específica de conflicto de negocio
            throw new ConflictoNegocioException(String.format(
                "No se puede cerrar el proyecto '%s': tiene %d tareas sin finalizar.",
                proyecto.getCodigo(), tareasPendientes
            ));
        }

        proyecto.setEstado(EstadoProyecto.FINALIZADO);
        proyectoRepository.save(proyecto);
    }
package com.ejemplo.gestor.tarea.controller;

import com.ejemplo.gestor.tarea.dto.CrearTareaRequest;
import com.ejemplo.gestor.tarea.dto.TareaResponse;
import com.ejemplo.gestor.tarea.service.TareaService;
import jakarta.validation.Valid;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/v1/proyectos/{proyectoId}/tareas")
public class TareaController {

    private final TareaService tareaService;

    public TareaController(TareaService tareaService) {
        this.tareaService = tareaService;
    }

    @PostMapping
    @PreAuthorize("hasAnyRole('JEFE_PROYECTO', 'ADMINISTRADOR')")
    public ResponseEntity<TareaResponse> crearTarea(
            @PathVariable Long proyectoId,
            @Valid @RequestBody CrearTareaRequest request) {

        TareaResponse response = tareaService.crearTarea(proyectoId, request);
        return ResponseEntity.status(HttpStatus.CREATED).body(response);
    }

    @GetMapping
    @PreAuthorize("isAuthenticated()")
    public ResponseEntity<Page<TareaResponse>> listarTareas(
            @PathVariable Long proyectoId,
            @PageableDefault(size = 10, sort = "prioridad") Pageable pageable) {

        return ResponseEntity.ok(tareaService.listarTareasProyecto(proyectoId, pageable));
    }
}

Paso 3 · Batería de pruebas en Bruno

  1. Creación dentro de presupuesto:
    • Lanza POST /api/v1/proyectos/1/tareas con coste de 5000.00 €.
    • Resultado: Código 201 Created.
  2. Prueba de estrés de regla de negocio (Exceso de presupuesto):
    • Lanza otra tarea con coste 200000.00 € sobre un proyecto que solo dispone de 150000.00 €.
    • Resultado: Código 400 Bad Request con detalle: "Presupuesto excedido. Presupuesto proyecto: 150000.00 €, Coste actual: 37000.00 €, Nueva tarea: 200000.00 €".
    • Ninguna fila queda guardada en la tabla tareas.
  3. Prueba de conflicto en cierre:
    • Lanza POST /api/v1/proyectos/1/cerrar.
    • Resultado: Código 409 Conflict indicando que existen tareas pendientes. El estado del proyecto permanece inalterado en EN_CURSO.

Paso 4 · Si algo no sale como dice el guion

Síntoma Causa casi segura Qué mirar
La regla de negocio se salta cuando llamas desde otro método del servicio Llamada interna: el proxy de @Transactional no interviene Extrae el método a otro bean, o llama siempre desde fuera
El rollback no revierte nada La excepción es comprobada (checked) Spring solo revierte ante RuntimeException salvo que declares rollbackFor
Dos peticiones simultáneas se saltan el límite de presupuesto Condición de carrera clásica Es exactamente el reto de esta sesión: bloqueo pesimista sobre la fila del proyecto
El 409 sale como 500 Falta el @ExceptionHandler de tu excepción de negocio Tu @RestControllerAdvice de la UD3 debe conocer la excepción nueva
LazyInitializationException al construir la respuesta Estás leyendo una relación fuera de la transacción Mapea a DTO dentro del servicio, no en el controlador

Paso 5 · Máquina de estados para Tareas

Escribe la tabla origen → destino permitido y úsala como referencia del código. Crea el DTO del cambio de estado con destino y, cuando proceda, motivo. En el servicio carga la tarea, comprueba permisos, valida la transición y solo entonces cambia sus campos. Conserva una excepción de dominio para el 409 y otra respuesta para una entrada mal formada. Prueba cada transición permitida y una prohibida desde su estado inicial correcto; no ejecutes toda la tabla sobre la misma tarea ya modificada.

Desarrollo II: seguridad e integración

Paso 6 · Bean de seguridad y llamadas salientes resilientes

Amplía la comprobación de propiedad existente en la UD9 o renómbrala mediante refactorización; no mantengas dos servicios con políticas contradictorias. Añade las nuevas expresiones a los métodos existentes y comprueba un usuario propietario y otro del mismo rol. En la integración externa reutiliza el bean RestClient con timeouts y el servicio con caché ya configurados; un bloque de ejemplo no debe crear otro cliente sin esos límites.

package com.ejemplo.gestor.core.security;

import com.ejemplo.gestor.proyecto.repository.ProyectoRepository;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;

@Service("seguridadService")
public class SeguridadService {

    private final ProyectoRepository proyectoRepository;

    public SeguridadService(ProyectoRepository proyectoRepository) {
        this.proyectoRepository = proyectoRepository;
    }

    public boolean esResponsableDeProyecto(Long proyectoId, Authentication authentication) {
        if (authentication == null || !authentication.isAuthenticated()) {
            return false;
        }

        // Los administradores tienen acceso maestro universal
        if (authentication.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_ADMINISTRADOR"))) {
            return true;
        }

        String username = authentication.getName();
        return proyectoRepository.findById(proyectoId)
            .map(p -> p.getResponsable().getUsername().equals(username))
            .orElse(false);
    }
}

Vinculamos la comprobación directamente en la anotación @PreAuthorize:

    @PutMapping("/{id}")
    @PreAuthorize("hasRole('ADMINISTRADOR') or (hasRole('JEFE_PROYECTO') and @seguridadService.esResponsableDeProyecto(#id, authentication))")
    public ResponseEntity<ProyectoDetalleResponse> actualizarProyecto(
            @PathVariable Long id,
            @Valid @RequestBody ActualizarProyectoRequest request) {

        return ResponseEntity.ok(proyectoService.actualizarProyecto(id, request));
    }

Conectamos la integración de la UD10 garantizando que el alta de incidencias nunca colapse ante averías de Open-Meteo:

package com.ejemplo.gestor.integration;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestClient;

@Service
public class ClimaService {

    private static final Logger log = LoggerFactory.getLogger(ClimaService.class);
    private final RestClient climaRestClient;

    // Inyectamos el bean openMeteoRestClient de la UD10: es el que trae la
    // factoría con connect-timeout y read-timeout ya configurados. Construir
    // aquí un cliente nuevo desde el Builder dejaría la llamada sin límite de
    // espera, y una API externa lenta bloquearía el hilo indefinidamente.
    public ClimaService(RestClient openMeteoRestClient) {
        this.climaRestClient = openMeteoRestClient;
    }

    @Cacheable(value = "climaProyectos", key = "#lat + '_' + #lon")
    public ClimaResultado consultarClimaSeguro(double lat, double lon) {
        try {
            log.info("Consultando Open-Meteo para coordenadas: lat={}, lon={}", lat, lon);

            var respuesta = climaRestClient.get()
                .uri("/v1/forecast?latitude={lat}&longitude={lon}&current_weather=true", lat, lon)
                .retrieve()
                .body(OpenMeteoResponse.class);

            return ClimaResultado.disponible(
                respuesta.currentWeather().temperature(),
                respuesta.currentWeather().weathercode()
            );

        } catch (ResourceAccessException ex) {
            log.warn("Timeout o fallo de red con Open-Meteo: {}. Aplicando degradación elegante.", ex.getMessage());
            return ClimaResultado.noDisponible("Servicio meteorológico fuera de línea temporalmente");
        } catch (Exception ex) {
            log.error("Error inesperado al consultar clima: {}. Se continúa sin enriquecimiento.", ex.getMessage());
            return ClimaResultado.noDisponible("Datos climáticos no disponibles");
        }
    }
}

Los adjuntos ya los sabes tratar: la sesión 43 dejó el almacenamiento con nombre saneado por UUID y validación de tipo MIME. Aquí solo hay que conectarlo al caso de uso y protegerlo como todo lo demás:

  1. Recupera de la UD10 tu AlmacenamientoService y el endpoint POST /api/v1/incidencias de tipo multipart/form-data.
  2. Protégelo con la misma regla de propiedad: solo el DESARROLLADOR asignado a la tarea, el JEFE_PROYECTO responsable o un ADMINISTRADOR pueden adjuntar un parte a una incidencia.
  3. Comprueba que el fichero se guarda con su UUID antes de consultar el clima, para que un fallo de Open-Meteo no deje un adjunto huérfano en disco sin fila en la base de datos.
Por qué la expresión lleva @ delante
En SpEL, @nombreDelBean busca un bean en el contexto de Spring. Por eso @Service("seguridadService") lleva el nombre escrito a mano: para que la expresión sea legible y no dependa de cómo Spring derive el nombre de la clase. Si cambias el nombre del bean y no el de la expresión, el fallo llega en tiempo de ejecución, no al compilar.
#id y authentication
#id es el parámetro del método anotado: solo existe si el método tiene un argumento llamado así. authentication es una variable que Spring Security pone siempre a tu disposición dentro de estas expresiones, con el usuario ya verificado.
El coste que estás aceptando
esResponsableDeProyecto hace una consulta a la base de datos antes de ejecutar el método, y otra dentro. Son dos viajes para una operación. Es asumible en una edición puntual, pero si lo pusieras en un listado tendrías el N+1 de la UD5 disfrazado de seguridad. Regla práctica: la seguridad por propiedad va en operaciones sobre un recurso, no sobre colecciones.
El atajo del administrador va primero, y no es casualidad
La comprobación de ROLE_ADMINISTRADOR está antes de tocar el repositorio: un administrador no paga la consulta. Ordenar las condiciones de más barata a más cara es lo que evita que la seguridad se convierta en el cuello de botella.

Paso 7 · Pruebas de matriz de permisos en Bruno

  1. Prueba de usurpación de proyecto (Caso no autorizado):
    • Autentícate como jefe2 (Elena).
    • Intenta modificar el proyecto PRJ-2026-001 cuyo responsable es jefe1 (Marcos): PUT http://localhost:8080/api/v1/proyectos/1.
    • Resultado esperado: Código 403 Forbidden. Spring Security bloquea la petición antes de ejecutar el servicio.
  2. Prueba como responsable legítimo:
    • Autentícate como jefe1.
    • Lanza el mismo PUT.
    • Resultado esperado: Código 200 OK.
  3. Prueba de degradación de red:
    • Apaga tu conexión WiFi o introduce coordenadas simuladas inalcanzables.
    • Da de alta una incidencia con fichero adjunto.
    • Resultado esperado: Código 201 Created. El informe se guarda con su archivo en disco y el campo clima reporta “Servicio meteorológico fuera de línea temporalmente”.

Paso 8 · Autorización granular en Tareas

Implementa la regla de propiedad para tareas:

  1. Añade a SeguridadService el método puedeModificarTarea(Long tareaId, Authentication auth).

  2. Permite la edición si el usuario es ADMINISTRADOR, o si es el JEFE_PROYECTO del proyecto padre, o si es el DESARROLLADOR que tiene asignada esa tarea.

  3. Protege el endpoint PATCH /api/v1/tareas/{id}/estado con esta comprobación.

  4. Prueba de la lectura ajena: comprueba también qué pasa cuando jefe2 lee el proyecto de jefe1. Decide si eso debe permitirse o no, anótalo, e impleméntalo. No hay respuesta única —en muchas organizaciones los proyectos son visibles para todos y solo la edición es privada—, pero tiene que ser una decisión tomada y no un descuido.

Paso 9 · Si algo no sale como dice el guion

Síntoma Causa casi segura Qué mirar
EL1008E: Property or field 'seguridadService' cannot be found El nombre del bean no coincide El de @Service("seguridadService") debe ser idéntico al de la expresión
Todos reciben 403, incluso el responsable La comparación falla ¿getResponsable() llega null por carga perezosa? Compara username, no objetos Usuario
LazyInitializationException dentro de SeguridadService Se accede al responsable fuera de la transacción Anota el método con @Transactional(readOnly = true), o usa una consulta con JOIN FETCH
El administrador recibe 403 La condición del atajo no encaja La autoridad almacenada es ROLE_ADMINISTRADOR, con prefijo; la comparación debe incluirlo
La regla no se aplica en absoluto Falta @EnableMethodSecurity Igual que en la sesión 38: sin esa anotación, @PreAuthorize es decoración
La incidencia se guarda sin adjunto cuando cae Open-Meteo Orden de operaciones equivocado Guarda el fichero y la fila antes de enriquecer con el clima, no al revés

Paso 10 · Comprobar y registrar el resultado del proyecto

  1. Comprueba las reglas nuevas, sus cambios atómicos y los rechazos por estado o propiedad. No debe quedar una operación parcialmente aplicada.
  2. Simula el fallo externo previsto y verifica que seguridad, persistencia y respuesta pública mantienen la política acordada para esa ampliación.

Ampliación si has completado el trabajo

Primero termina y verifica los pasos anteriores. Estos retos profundizan en el mismo contenido; no sustituyen la entrega ni obligan a iniciar otro proyecto.

Reto · Control de concurrencia pesimista en presupuestos

Si dos usuarios añaden tareas simultáneamente al mismo proyecto en milisegundos idénticos, ambos podrían leer el mismo coste actual acumulado antes de que el otro guarde su fila (Race Condition), superando el presupuesto total.

Investiga cómo resolver esta condición de carrera:

  1. Utiliza @Lock(LockModeType.PESSIMISTIC_WRITE) en la consulta de búsqueda de Proyecto para bloquear la fila en PostgreSQL durante la transacción.
  2. Comprueba mediante un test concurrente multihilo que las dos creaciones se serializan y la segunda es rechazada correctamente por falta de saldo.
Objetivo mínimoRelación Proyecto-Tarea operativa con consultas paginadas y respuesta 201.
Si lo tienesCálculo atómico de techo presupuestario y rechazo 409 al cerrar con tareas pendientes.
RetoBloqueo pesimista (PESSIMISTIC_WRITE) para blindar el presupuesto ante concurrencia extrema.
Ver respuestas

1 · Porque la base de datos procesa millones de filas de forma indexada en milisegundos y devuelve solo un número decimal por la red, mientras que iterar en Java exige transferir miles de entidades y saturar la memoria RAM.

2 · El código 409 Conflict (indica que la petición no puede procesarse debido a un conflicto con el estado actual del recurso).

3 · Para garantizar la atomicidad y el aislamiento ACID: si la comprobación pasa y la tarea se guarda, la operación se confirma; si algo falla, no se modifica la base de datos.

4 · Permite definir valores por defecto sensatos (tamaño de página, campo de ordenación y dirección ascendente/descendente) si el cliente no envía los parámetros en la URL.

Reto · Auditoría de accesos denegados en base de datos

Cada vez que un usuario recibe un código 403 Forbidden puede tratarse de un error inocente o de un ataque malicioso de fuerza bruta / enumeración de IDs.

  1. Implementa un listener para el evento de Spring Security AuthorizationFailureEvent.
  2. Registra en una tabla auditoria_seguridad el usuario, la IP del cliente, la URL intentada y el motivo de denegación.
Objetivo mínimoSeguridad JWT activa en endpoints y cliente de clima con captura de excepciones.
Si lo tienesEvaluador SpEL esResponsableDeProyecto protegiendo recursos frente a accesos ajenos.
RetoAuditoría reactiva de eventos AuthorizationFailureEvent persistida en base de datos.
Ver respuestas

1 · Porque violaría el principio de aislamiento y confidencialidad; cada responsable solo debe gestionar los proyectos y presupuestos formalmente asignados a su cargo.

2 · Proporciona la instancia actual de Authentication del SecurityContext, con el nombre del usuario (getName()), sus roles/autoridades (getAuthorities()) y sus credenciales.

3 · Evita abortar un caso de uso principal válido del usuario (como guardar un informe o incidencia) por culpa de un servicio secundario complementario que no está disponible.

4 · 401 Unauthorized significa que el cliente no se ha identificado (falta el token o es inválido); 403 Forbidden significa que el servidor sabe quién es el usuario pero sus permisos son insuficientes para esa acción.

Cierre

15 minutos · resultado comprobable y explicación individual

Al terminar la sesión:

El backend satisface los criterios del producto sin depender de que exista ya una pantalla para cada operación.

Cada integrante explica una decisión del código apoyándose en una de las comprobaciones realizadas.