← Sesión, autenticación y Spring Security

Sesión 38 · Semana 19

Permisos sobre cada recurso

Proyecto compartido. En el taller de Intermodular que abre esta semana has trabajado planificar permisos y preparar el entorno de seguridad. En Servidor continúas la implementación del mismo producto.

Se explica

25 minutos · explicación y demostración

El usuario ya se autentica contra PostgreSQL. Hoy aplicarás permisos sobre recursos concretos. RBAC significa permisos basados en roles; una comprobación de propiedad añade la relación entre el usuario y el registro solicitado. Conocer un id no concede acceso a ese recurso.

Roles globales frente a Permisos atómicos

En aplicaciones en crecimiento existen dos formas de modelar la autorización:

Roles vs Permisos en Spring Security
  1. Usuario
  2. Roles (ROLE_ADMINISTRADOR, ROLE_DESARROLLADOR)
  3. Permisos Atómicos (PROYECTO_BORRAR, TAREA_EDITAR)
  4. Operación protegida
  • Roles (hasRole): Representan el cargo de una persona en la organización. Por convención en Spring Security llevan el prefijo ROLE_ en la base de datos, pero en el código se evalúan sin él:
    • hasRole('ADMINISTRADOR') comprueba internamente si el usuario posee la autoridad ROLE_ADMINISTRADOR.
  • Autoridades / Permisos atómicos (hasAuthority): Representan una acción puntual sobre un recurso (PROYECTO_WRITE, TAREA_DELETE, INFORME_EXPORTAR).
    • Permiten construir sistemas de permisos ultra-flexibles donde los roles son agrupaciones de permisos configurables en base de datos.

Seguridad en rutas URL frente a Seguridad en métodos con @PreAuthorize

Podemos aplicar reglas de autorización en dos capas complementarias:

Estrategia Dónde se define Sintaxis típica Ventajas y uso recomendado
Seguridad de Rutas (HTTP Filter) En SecurityConfig dentro de SecurityFilterChain. .requestMatchers(HttpMethod.DELETE, "/api/v1/proyectos/**").hasRole("ADMINISTRADOR") Primera barrera perimetral: rechaza peticiones no autorizadas antes de que lleguen al controlador.
Seguridad de Métodos (@PreAuthorize) Sobre métodos de controladores o clases @Service. @PreAuthorize("hasRole('ADMINISTRADOR')")
@PreAuthorize("hasRole('DEV') and #tarea.autor == authentication.name")
Seguridad en profundidad: permite evaluar reglas de negocio complejas, parámetros del método (#id) y expresiones de propiedad (SpEL).

La falacia de la seguridad por ocultación

Uno de los errores más peligrosos de los desarrolladores que vienen del mundo frontend es pensar que la seguridad consiste en esto:

<!-- CUIDADO: Esto es experiencia de usuario, NO es seguridad -->
<button *ngIf="usuario.rol === 'ADMIN'" (click)="eliminarProyecto(id)">
  Eliminar proyecto
</button>

Ocultar ese botón es una buena práctica de diseño de interfaces: a un usuario normal no le muestras botones que no puede usar.

Pero confundir eso con seguridad es un error catastrófico:

  • Cualquier usuario puede abrir la consola de DevTools (F12), inspeccionar el DOM y eliminar el atributo disabled o hacer visible el botón en 3 segundos.
  • Cualquier usuario puede abrir Bruno, Postman o una consola con curl y lanzar directamente un DELETE http://localhost:8080/api/v1/proyectos/1.

La ley del servidor como frontera única

El cliente web es un entorno bajo el control absoluto del usuario (y del atacante).

La única frontera real de seguridad de un sistema es el backend. Todo endpoint debe comprobar permisos en el servidor en cada petición, asumiendo siempre que el cliente puede ser malicioso.

Pruebas de seguridad con MockMvc y @WithMockUser

Para garantizar que nuestros endpoints están blindados y que ningún refactor futuro rompa las reglas de seguridad, escribimos pruebas automáticas con @WithMockUser:

@Test
@WithMockUser(username = "dev1", roles = {"DESARROLLADOR"})
void eliminarProyecto_conRolDesarrollador_devuelve403Forbidden() throws Exception {
    mockMvc.perform(delete("/api/v1/proyectos/1"))
        .andExpect(status().isForbidden());
}

La anotación @WithMockUser:

  • Inyecta un Authentication en el SecurityContextHolder antes de que el filtro de seguridad ejecute la petición.
  • Permite probar autorizaciones (hasRole, @PreAuthorize) de forma instantánea sin necesidad de crear usuarios en PostgreSQL ni generar hashes BCrypt.

Se trabaja

140 minutos · implementación guiada sobre el proyecto propio

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

  1. Abre la matriz de permisos, los métodos del servicio y SecurityConfig. Prepara dos usuarios del mismo rol y un recurso que pertenezca solo a uno.
  2. Añade una cuenta con otro rol para contrastar permisos de función. Guarda peticiones separadas por identidad para evitar reutilizar credenciales sin darte cuenta.
  3. Localiza las pruebas de controlador y la dependencia de test de seguridad que se añade en el taller.

Paso 2 · Implementar la Matriz RBAC en la aplicación

Si ManejadorDeErrores conserva un método para Exception, añade otro para denegaciones de Spring Security, importando org.springframework.security.access.AccessDeniedException. Así un permiso rechazado dentro del controlador mantiene el estado 403.

@ExceptionHandler(AccessDeniedException.class)
public ProblemDetail permisoDenegado(AccessDeniedException ex) {
    return ProblemDetail.forStatusAndDetail(HttpStatus.FORBIDDEN,
        "No tienes permiso para realizar esta operación");
}

Las peticiones rechazadas antes del controlador se responden desde la cadena de seguridad; las configuraremos allí al integrar JWT.

Añade @EnableMethodSecurity a la SecurityConfig existente e incorpora las reglas conservando las rutas públicas ya acordadas. En los controladores añade @PreAuthorize sobre los métodos existentes, sin sustituir sus cuerpos por los puntos suspensivos del esquema. Crea después TareaSecurityService en un paquete bajo el principal; su nombre de bean debe coincidir con la expresión. En TareaController, que ya tiene el prefijo /api/v1/tareas, la anotación de modificación utiliza /{id}, no /tareas/{id}.

package com.ejemplo.gestor.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity // Habilita anotaciones @PreAuthorize y @Secured en toda la aplicación
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                // Lo único público sigue siendo la documentación
                .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()

                // Borrar un proyecto es la operación irreversible de la matriz
                .requestMatchers(HttpMethod.DELETE, "/api/v1/proyectos/**").hasRole("ADMINISTRADOR")

                // Gestión de usuarios: reservada estrictamente a Administradores
                .requestMatchers("/api/v1/usuarios/**").hasRole("ADMINISTRADOR")

                // Todo lo demás requiere que el usuario esté al menos autenticado
                .anyRequest().authenticated()
            )
            .httpBasic(Customizer.withDefaults());

        return http.build();
    }
}
Por qué hay dos sitios donde escribir reglas
El filterChain decide por ruta y método HTTP, antes de que Spring sepa siquiera qué controlador va a atender la petición. @PreAuthorize decide por método Java, cuando ya conoce los argumentos. La primera es una valla perimetral; la segunda, una cerradura en cada puerta.
Cuál usar
Todo lo que se pueda expresar como «esta ruta con este verbo es solo para este rol» va en el filterChain: se rechaza antes y en un solo sitio. En cuanto la regla necesita mirar el dato concreto —«solo si esta tarea es tuya»— no hay ruta que la exprese y hace falta @PreAuthorize.
Qué pasa si las dos hablan de lo mismo
Se aplican las dos, y gana la más restrictiva, porque la del filtro se evalúa primero e interrumpe la cadena. Mantener ambas no constituye un error, sino defensa en profundidad; conserva no obstante una sola como fuente de verdad para cada regla, o acabarás cambiando una y no la otra.
El prefijo ROLE_, de una vez
En la base de datos guardas ROLE_ADMINISTRADOR. En hasRole() escribes 'ADMINISTRADOR', sin prefijo, porque el método lo añade solo. Si escribes hasRole('ROLE_ADMINISTRADOR'), Spring buscará ROLE_ROLE_ADMINISTRADOR y nadie pasará nunca. La versión sin magia es hasAuthority('ROLE_ADMINISTRADOR'), que compara literalmente.

Decoramos los métodos de escritura con anotaciones declarativas:

@RestController
@RequestMapping("/api/v1/proyectos")
public class ProyectoController {

    private final ProyectoService proyectoService;

    public ProyectoController(ProyectoService proyectoService) {
        this.proyectoService = proyectoService;
    }

    // Creación permitida a Jefes de Proyecto y Administradores
    @PostMapping
    @PreAuthorize("hasAnyRole('JEFE_PROYECTO', 'ADMINISTRADOR')")
    public ResponseEntity<ProyectoResponse> crear(@Valid @RequestBody ProyectoRequest request) {
        // ...
    }

    // Borrado de proyectos: reservado exclusivamente a Administradores
    @DeleteMapping("/{id}")
    @PreAuthorize("hasRole('ADMINISTRADOR')")
    public ResponseEntity<Void> eliminar(@PathVariable Long id) {
        proyectoService.eliminarProyecto(id);
        return ResponseEntity.noContent().build();
    }
}

Podemos condicionar la edición de una tarea a que el usuario sea el autor de la misma:

    // El usuario solo puede editar la tarea si es Administrador O si él mismo es el asignado
    @PutMapping("/{id}")
    @PreAuthorize("hasRole('ADMINISTRADOR') or @tareaSecurityService.esAsignado(#id, authentication.name)")
    public ResponseEntity<TareaResponse> actualizarTarea(
            @PathVariable Long id,
            @Valid @RequestBody TareaRequest request) {
        // ...
    }

Donde tareaSecurityService es un componente Spring que comprueba la base de datos:

@Component
public class TareaSecurityService {
    private final TareaRepository tareaRepository;

    public TareaSecurityService(TareaRepository tareaRepository) {
        this.tareaRepository = tareaRepository;
    }

    public boolean esAsignado(Long tareaId, String username) {
        return tareaRepository.findById(tareaId)
            .map(t -> t.getAsignadoA() != null && t.getAsignadoA().getUsername().equals(username))
            .orElse(false);
    }
}

Paso 3 · Simulación de matriz de permisos en Bruno

Con los usuarios cargados en PostgreSQL (admin con ROLE_ADMINISTRADOR y dev1 con ROLE_DESARROLLADOR), ejecuta estas pruebas en Bruno:

  1. Borrado por Administrador:
    • Petición: DELETE /api/v1/proyectos/1.
    • Auth: Basic con admin / Password123!.
    • Resultado esperado: Código 204 No Content. Operación permitida.
  2. Borrado fraudulento por Desarrollador:
    • Misma petición: DELETE /api/v1/proyectos/1.
    • Auth: Basic con dev1 / Password123!.
    • Resultado esperado: Código 403 Forbidden. Spring Security intercepta la llamada, comprueba que dev1 carece de ROLE_ADMINISTRADOR y deniega el acceso sin ejecutar el método del controlador.
  3. Acceso anónimo a la misma ruta:
    • Misma petición sin credenciales en la pestaña Auth.
    • Resultado esperado: Código 401 Unauthorized.

Paso 4 · Si algo no sale como dice el guion

Síntoma Causa casi segura Qué mirar
@PreAuthorize no hace absolutamente nada Falta @EnableMethodSecurity Va sobre la clase SecurityConfig, junto a @EnableWebSecurity
Todo el mundo recibe 403, incluso admin Prefijo duplicado ¿Has escrito hasRole('ROLE_ADMINISTRADOR')? Quita el ROLE_
admin recibe 403 y el rol está bien escrito La autoridad guardada no lleva el prefijo En data.sql la columna debe decir ROLE_ADMINISTRADOR, no ADMINISTRADOR
EL1008E: Property or field 'tareaSecurityService' cannot be found El bean no existe con ese nombre El nombre en la expresión es el del bean: @Component sobre TareaSecurityService lo registra como tareaSecurityService, con minúscula inicial
El 403 llega, pero el método se ejecutó igualmente Estás anotando un método privado, o llamándolo desde la misma clase Las anotaciones de seguridad funcionan por proxy: solo actúan en llamadas públicas que entran desde fuera del bean

Paso 5 · Trasladar la matriz entera al código

Hasta ahora la matriz de la sesión 35 era un documento. Aquí se convierte en código ejecutable.

  1. Permite que ROLE_DESARROLLADOR, ROLE_JEFE_PROYECTO y ROLE_ADMINISTRADOR puedan crear tareas sobre un proyecto existente (POST /api/v1/proyectos/{id}/tareas).
  2. Restringe el borrado de tareas (DELETE /api/v1/tareas/{id}) a ROLE_JEFE_PROYECTO y ROLE_ADMINISTRADOR.
  3. Aplica la fila más incómoda de la matriz: POST /api/v1/proyectos es de JEFE_PROYECTO y ADMINISTRADOR, pero DELETE /api/v1/proyectos/{id} es solo de ADMINISTRADOR. Un JEFE_PROYECTO que borra debe recibir 403, no 204.
  4. Decide, y anota por qué, dónde pones cada una de esas tres reglas: en el filterChain o en @PreAuthorize. No hay una respuesta única, pero sí tiene que haber un criterio.
  5. Comprueba con tu cliente HTTP la matriz completa: son 9 filas × 4 columnas = 36 comprobaciones. Guárdalas en la carpeta 09-seguridad con un nombre que diga qué esperas, del tipo dev1-borra-proyecto-403.
  6. Marca en la tabla de la sesión 35, con un ✔, cada casilla que ya devuelve lo que decía. Las que no coincidan son tu lista de tareas: o está mal el código, o está mal la matriz, y decidir cuál de las dos es parte del ejercicio.
Cómo saber que lo has terminado
Las 36 casillas de la matriz responden lo que la tabla dice. En particular: un DESARROLLADOR nunca ve un 401 (ya está identificado, sus rechazos son 403), y un JEFE_PROYECTO puede crear proyectos pero no borrarlos.

Proteger endpoints

Paso 6 · Batería de tests de seguridad para ProyectoController

Añade spring-security-test con scope test una sola vez. Crea la clase de pruebas, importa SecurityConfig y declara mocks para todos los colaboradores de su controlador. Copia los métodos de test dentro de esa clase. @WithMockUser prepara la identidad; no crea una fila de usuario en PostgreSQL. Cuando pruebes reglas de propiedad, prepara además la respuesta del colaborador que comprueba esa propiedad y verifica que un usuario distinto obtiene el rechazo esperado.

@WithMockUser no viene con el starter de test. Añade a tu pom.xml:

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
</dependency>
package com.ejemplo.gestor;

import com.ejemplo.gestor.config.SecurityConfig;
import com.ejemplo.gestor.controller.ProyectoController;
import com.ejemplo.gestor.service.ProyectoService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;

import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@WebMvcTest(ProyectoController.class)
@Import(SecurityConfig.class)   // sin esta línea, tus reglas NO se aplican
class ProyectoSecurityTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private ProyectoService proyectoService;

La línea que decide si este test sirve para algo

@WebMvcTest carga controladores, no clases de configuración cualesquiera. Tu SecurityConfig es una @Configuration normal, así que no entra: el test se ejecutaría contra la cadena de seguridad por defecto de Spring Boot, sin tus requestMatchers y sin @EnableMethodSecurity.

El resultado es el peor posible: el test de 401 pasa igualmente (la cadena por defecto también exige autenticación), pero el de 403 devuelve 204 y falla, o peor, pasa por una razón equivocada. Tendrías una suite verde que no está probando tus reglas. @Import(SecurityConfig.class) es lo que hace que el test hable de tu configuración y no de otra.

Verificamos que si no hay identidad en el contexto, el peralte de seguridad intercepta la llamada:

    @Test
    void eliminarProyecto_sinAutenticar_devuelve401Unauthorized() throws Exception {
        mockMvc.perform(delete("/api/v1/proyectos/1"))
            .andExpect(status().isUnauthorized());

        // Verificamos que la lógica de negocio jamás fue invocada
        verify(proyectoService, never()).eliminarProyecto(anyLong());
    }

Verificamos que un usuario identificado con rol DESARROLLADOR recibe 403:

    @Test
    @WithMockUser(username = "juan.dev", roles = {"DESARROLLADOR"})
    void eliminarProyecto_conRolDesarrollador_devuelve403Forbidden() throws Exception {
        mockMvc.perform(delete("/api/v1/proyectos/1"))
            .andExpect(status().isForbidden());

        verify(proyectoService, never()).eliminarProyecto(anyLong());
    }

Verificamos que el rol ADMINISTRADOR ejecuta la acción con éxito:

    @Test
    @WithMockUser(username = "admin.jefe", roles = {"ADMINISTRADOR"})
    void eliminarProyecto_conRolAdministrador_devuelve204NoContent() throws Exception {
        doNothing().when(proyectoService).eliminarProyecto(1L);

        mockMvc.perform(delete("/api/v1/proyectos/1"))
            .andExpect(status().isNoContent());

        verify(proyectoService, times(1)).eliminarProyecto(1L);
    }
}

Paso 7 · Ejecutar la suite de seguridad en terminal

Ejecuta las pruebas desde la consola de Maven:

./mvnw test -Dtest=ProyectoSecurityTest

Comprueba en la salida:

  • Los 3 tests pasan al 100 % en verde en menos de 1 segundo.
  • Queda demostrado que la interfaz de cliente no condiciona la autorización: un usuario no administrador no puede borrar un proyecto en el servidor.

Un test de seguridad que nunca ha fallado no ha demostrado nada todavía. Rómpelo a propósito y míralo caer:

  1. Comenta la línea @PreAuthorize("hasRole('ADMINISTRADOR')") del método eliminar.
  2. Ejecuta ./mvnw test -Dtest=ProyectoSecurityTest.
  3. Resultado esperado: el test del 403 falla con Status expected:<403> but was:<204>. Ahí está la regresión que este test existe para cazar.
  4. Descomenta la anotación y vuelve a ejecutar. Verde otra vez.
  5. Repite la jugada quitando @Import(SecurityConfig.class) de la clase de test. Verás el mismo fallo, y esa es la lección: un test verde solo vale si estás seguro de contra qué configuración corre.

Paso 8 · Si algo no sale como dice el guion

Síntoma Causa casi segura Qué mirar
cannot find symbol: class WithMockUser Falta la dependencia spring-security-test con <scope>test</scope> en el pom.xml
El test de 403 recibe 204 Tus reglas no están cargadas Falta @Import(SecurityConfig.class), o falta @EnableMethodSecurity en SecurityConfig
El test de 401 recibe 403 El usuario anónimo se considera autenticado ¿Has puesto @WithMockUser a nivel de clase? Solo debe estar en los métodos que lo necesitan
POST y DELETE reciben 403 en todos los tests CSRF activo dentro del test O usas .with(csrf()) en la petición, o mantienes csrf.disable() en la config que importas
No qualifying bean of type UserDetailsService Tu SecurityConfig arrastra dependencias que el slice no carga Añade @MockBean private CustomUserDetailsService userDetailsService; a la clase de test

Paso 9 · Batería de tests de seguridad para Tareas

Aplica el mismo patrón para proteger la creación y modificación de tareas:

  1. Crea TareaSecurityTest con @WebMvcTest(TareaController.class) y @Import(SecurityConfig.class).
  2. Escribe un test que verifique que un usuario anónimo recibe 401 al intentar crear una tarea (POST /api/v1/proyectos/1/tareas).
  3. Escribe un test con @WithMockUser(roles = "DESARROLLADOR") que confirme que un desarrollador sí puede crear tareas (código 201).
  4. Escribe un test que verifique que el borrado de tareas devuelve 403 para @WithMockUser(roles = "DESARROLLADOR") y 204 para @WithMockUser(roles = "JEFE_PROYECTO").
  5. Añade a todos los tests de rechazo la verificación verify(tareaService, never()).…. Comprobar el código de estado demuestra que el cliente recibió un 403; comprobar que el servicio nunca se llamó demuestra que la operación no llegó a ocurrir. No son lo mismo, y solo la segunda descarta un borrado que sucedió y luego respondió mal.
  6. Cuenta cuántas casillas de la matriz de la sesión 35 cubre ya tu suite. Si has hecho los pasos 2 a 4, son 6 de 36. Anota en tu cuaderno cuáles faltan: la sesión 45 va a partir de ese inventario.
Cómo saber que lo has terminado
./mvnw test pasa en verde; cada test de rechazo verifica además que el servicio no se invocó; y has visto al menos un test tuyo fallar en rojo al quitarle la anotación de seguridad que protege.

Paso 10 · Comprobar y registrar el resultado del proyecto

  1. Prueba una acción como propietario y como otro usuario del mismo rol. Comprueba tanto lectura como modificación y que los rechazos no alteran datos.
  2. Ejecuta los tests de identidad ausente, rol insuficiente y acceso permitido; sus expectativas deben reflejar tu matriz y la política de errores documentada.

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 · Excepciones de acceso denegado personalizadas

Por defecto, cuando un usuario autenticado recibe un 403 Forbidden, Spring Security no devuelve un formato amigable.

Implementa un AccessDeniedHandler personalizado:

  1. Crea la clase CustomAccessDeniedHandler que implemente AccessDeniedHandler.
  2. Emite una respuesta estándar RFC 7807 con código 403, título “Acceso Denegado” y detalle indicando que el rol actual no dispone de los privilegios requeridos.
  3. Regístralo en SecurityConfig bajo .exceptionHandling(ex -> ex.accessDeniedHandler(...)).

Formato de entrega

Incluye esta explicación en el registro de la sesión dentro del repositorio de GitHub, junto al código y las comprobaciones. La entrega es el enlace al repositorio y al commit de la sesión.

Objetivo mínimoReglas de autorización por rol configuradas en SecurityFilterChain y probadas con Bruno.
Si lo tienesAnotación @PreAuthorize aplicada en controladores con hasRole y hasAnyRole diferenciando 401 y 403.
RetoExpresiones SpEL para seguridad a nivel de fila y AccessDeniedHandler emitiendo respuestas RFC 7807 ante 403.
Ver respuestas

1 · Porque Spring Security añade automáticamente el prefijo ROLE_ por convención histórica para diferenciar roles de permisos simples.

2 · Permite colocar la regla de seguridad junto al método que ejecuta la acción, facilitando la legibilidad, y permite acceder a los parámetros del método y a la lógica de dominio.

3 · Código HTTP 403 Forbidden.

4 · Para hacer referencia a un argumento formal que recibe el método anotado (evaluación contextual de parámetros).

Reto · Pruebas de seguridad basadas en atributos con SpEL

En el trabajo anterior definimos que un desarrollador solo puede editar las tareas que tiene asignadas a su nombre.

¿Cómo se prueba esa regla con MockMvc?

  1. Escribe un test simulando a @WithMockUser(username = "carlos").
  2. Simula una tarea cuyo responsable es "maria".
  3. Verifica que al intentar hacer PUT /api/v1/tareas/10 el sistema responde con 403 Forbidden.
  4. Repite la prueba con una tarea asignada a "carlos" y confirma que responde con 200 OK.
Objetivo mínimoTests de seguridad con @WebMvcTest y @WithMockUser verificando casos 401, 403 y 204.
Si lo tienesSuite completa de proyectos y tareas cubriendo la matriz RBAC completa con aserciones estrictas.
RetoTests de autorización a nivel de fila (SpEL) con simulación de propiedad de recursos verificados.
Ver respuestas

1 · Porque el cliente corre en el dispositivo del usuario, quien puede inspeccionar el DOM, modificar el script o lanzar peticiones HTTP directas por consola evadiendo cualquier restricción visual.

2 · El test normal evalúa rutas y serialización; @WithMockUser inyecta un contexto de seguridad previo para comprobar si los filtros y @PreAuthorize permiten o bloquean la petición.

3 · Para demostrar fehacientemente que la seguridad interceptó la llamada en la frontera de red y que ninguna lógica de negocio llegó a ejecutarse en el servidor.

4 · Mediante el atributo roles = {"ROL_A", "ROL_B"} dentro de la anotación.

Cierre

15 minutos · resultado comprobable y explicación individual

Al terminar la sesión:

Los tests demuestran que un usuario no lee ni modifica recursos ajenos fuera de la política del producto.

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