View Javadoc
1   package fr.tiogars.chat.config;
2   
3   import static org.mockito.Mockito.verifyNoInteractions;
4   import static org.mockito.Mockito.when;
5   import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
6   import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
7   import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
8   
9   import java.nio.charset.StandardCharsets;
10  import java.util.List;
11  import java.util.Optional;
12  
13  import org.junit.jupiter.api.BeforeEach;
14  import org.junit.jupiter.api.Test;
15  import org.junit.jupiter.params.ParameterizedTest;
16  import org.junit.jupiter.params.provider.ValueSource;
17  import org.springframework.beans.factory.annotation.Autowired;
18  import org.springframework.boot.test.context.TestConfiguration;
19  import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
20  import org.springframework.context.annotation.Import;
21  import org.springframework.http.MediaType;
22  import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
23  import org.springframework.security.core.userdetails.User;
24  import org.springframework.security.core.userdetails.UserDetails;
25  import org.springframework.security.core.userdetails.UserDetailsService;
26  import org.springframework.security.web.FilterChainProxy;
27  import org.springframework.test.context.bean.override.mockito.MockitoBean;
28  import org.springframework.test.web.servlet.MockMvc;
29  import org.springframework.test.web.servlet.setup.MockMvcBuilders;
30  import org.springframework.web.context.WebApplicationContext;
31  
32  import fr.tiogars.chat.domains.auth.services.JwtTokenService;
33  import fr.tiogars.chat.domains.auth.services.JwtAuthenticationFilter;
34  import fr.tiogars.chat.domains.task.controllers.TaskPrintController;
35  import fr.tiogars.chat.domains.task.services.TaskPrintService;
36  import fr.tiogars.chat.domains.task.services.TasksPrintAllService;
37  import fr.tiogars.chat.domains.task.services.TasksPrintSelectedService;
38  
39  @WebMvcTest({TaskPrintController.class,
40          fr.tiogars.chat.domains.tag.controllers.TagController.class,
41          fr.tiogars.chat.domains.nomenclature.controllers.NomenclatureController.class})
42  @Import({SecurityConfiguration.class, JwtAuthenticationFilter.class,
43          TaskPrintSecurityTest.SecurityTestConfiguration.class})
44  class TaskPrintSecurityTest {
45  
46      @TestConfiguration
47      @EnableWebSecurity
48      static class SecurityTestConfiguration {
49      }
50  
51      @Autowired
52      private WebApplicationContext context;
53  
54      @Autowired
55      private FilterChainProxy springSecurityFilterChain;
56  
57      private MockMvc mvc;
58  
59      @MockitoBean
60      private JwtTokenService jwtTokenService;
61  
62      @MockitoBean
63      private UserDetailsService userDetailsService;
64  
65      @MockitoBean
66      private TaskPrintService printService;
67  
68      @MockitoBean
69      private TasksPrintAllService printAllService;
70  
71      @MockitoBean
72      private TasksPrintSelectedService printSelectedService;
73  
74      @MockitoBean
75      private fr.tiogars.chat.domains.taxonomy.TaxonomyService taxonomyService;
76  
77      @ParameterizedTest
78      @ValueSource(strings = {"/api/v1/tag", "/api/v1/nomenclature"})
79      void taxonomyReadsAllowUsersButWritesRequireAdmin(String path) throws Exception {
80          authenticate("USER");
81          mvc.perform(get(path).header("Authorization", "Bearer " + "print-test")).andExpect(status().isOk());
82          mvc.perform(post(path).header("Authorization", "Bearer " + "print-test")
83                  .contentType(MediaType.APPLICATION_JSON).content("{\"code\":\"CODE\",\"label\":\"Label\"}"))
84                  .andExpect(status().isForbidden());
85          mvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete(path + "/1")
86                  .header("Authorization", "Bearer " + "print-test")).andExpect(status().isForbidden());
87      }
88  
89      @ParameterizedTest
90      @ValueSource(strings = {"/api/v1/tag", "/api/v1/nomenclature"})
91      void taxonomyWritesAllowAdminAndAnonymousReadsAreDenied(String path) throws Exception {
92          mvc.perform(get(path)).andExpect(status().isForbidden());
93          authenticate("ADMIN");
94          mvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete(path + "/1")
95                  .header("Authorization", "Bearer " + "print-test")).andExpect(status().isNoContent());
96      }
97  
98      @ParameterizedTest
99      @ValueSource(strings = {"/api/v1/tag", "/api/v1/nomenclature"})
100     void taxonomyRejectsAuthenticatedUnrelatedRolesForEveryCrudOperation(String path) throws Exception {
101         authenticate("ANONYMOUS");
102         for (String readPath : List.of(path, path + "/1")) {
103             mvc.perform(get(readPath).header("Authorization", "Bearer " + "print-test"))
104                     .andExpect(status().isForbidden());
105         }
106         mvc.perform(post(path).header("Authorization", "Bearer " + "print-test")
107                 .contentType(MediaType.APPLICATION_JSON).content("{\"code\":\"CODE\",\"label\":\"Label\"}"))
108                 .andExpect(status().isForbidden());
109         mvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put(path + "/1")
110                 .header("Authorization", "Bearer " + "print-test")
111                 .contentType(MediaType.APPLICATION_JSON).content("{\"label\":\"Label\"}"))
112                 .andExpect(status().isForbidden());
113         mvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete(path + "/1")
114                 .header("Authorization", "Bearer " + "print-test")).andExpect(status().isForbidden());
115         verifyNoInteractions(taxonomyService);
116     }
117 
118     @BeforeEach
119     void configureSecurityFilters() {
120         // Register JWT only inside its security chain, not twice through MockMvc's bean filter discovery.
121         mvc = MockMvcBuilders.webAppContextSetup(context).addFilters(springSecurityFilterChain).build();
122     }
123 
124     @ParameterizedTest
125     @ValueSource(strings = {"USER", "ADMIN"})
126     void printEndpoints_should_allowTheSameRolesAsReading(String role) throws Exception {
127         authenticate(role);
128         byte[] pdf = "%PDF-1.7".getBytes(StandardCharsets.UTF_8);
129         when(printService.print(42L)).thenReturn(Optional.of(pdf));
130         when(printAllService.printAll()).thenReturn(pdf);
131         when(printSelectedService.printSelected(List.of(42L))).thenReturn(Optional.of(pdf));
132 
133         mvc.perform(get("/api/v1/task/42/print.pdf")
134                 .header("Authorization", "Bearer print-test")).andExpect(status().isOk());
135         mvc.perform(get("/api/v1/task/print.pdf")
136                 .header("Authorization", "Bearer print-test")).andExpect(status().isOk());
137         mvc.perform(post("/api/v1/task/print-selected.pdf")
138                 .header("Authorization", "Bearer print-test")
139                 .contentType(MediaType.APPLICATION_JSON).content("{\"ids\":[42]}")).andExpect(status().isOk());
140     }
141 
142     @Test
143     void printEndpoints_should_denyUnauthenticatedRequests() throws Exception {
144         mvc.perform(get("/api/v1/task/42/print.pdf")).andExpect(status().isForbidden());
145         mvc.perform(get("/api/v1/task/print.pdf")).andExpect(status().isForbidden());
146         mvc.perform(post("/api/v1/task/print-selected.pdf")
147                 .contentType(MediaType.APPLICATION_JSON).content("{\"ids\":[42]}")).andExpect(status().isForbidden());
148 
149         verifyNoInteractions(printService, printAllService, printSelectedService);
150     }
151 
152     @Test
153     void printEndpoints_should_denyAuthenticatedUsersWithoutReadingRoles() throws Exception {
154         authenticate("ANONYMOUS");
155         mvc.perform(get("/api/v1/task/42/print.pdf")
156                 .header("Authorization", "Bearer print-test")).andExpect(status().isForbidden());
157         mvc.perform(get("/api/v1/task/print.pdf")
158                 .header("Authorization", "Bearer print-test")).andExpect(status().isForbidden());
159         mvc.perform(post("/api/v1/task/print-selected.pdf")
160                 .header("Authorization", "Bearer print-test")
161                 .contentType(MediaType.APPLICATION_JSON).content("{\"ids\":[42]}")).andExpect(status().isForbidden());
162 
163         verifyNoInteractions(printService, printAllService, printSelectedService);
164     }
165 
166     private void authenticate(String role) {
167         UserDetails user = User.withUsername("printer").password("unused").roles(role).build();
168         when(jwtTokenService.extractUsername("print-test")).thenReturn(Optional.of("printer"));
169         when(userDetailsService.loadUserByUsername("printer")).thenReturn(user);
170         when(jwtTokenService.isValid("print-test", user)).thenReturn(true);
171     }
172 }