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
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 }