diff --git a/src/main/java/org/wise/portal/spring/impl/WebSecurityConfig.java b/src/main/java/org/wise/portal/spring/impl/WebSecurityConfig.java index 30e55827e..f56c342f3 100644 --- a/src/main/java/org/wise/portal/spring/impl/WebSecurityConfig.java +++ b/src/main/java/org/wise/portal/spring/impl/WebSecurityConfig.java @@ -29,7 +29,6 @@ import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSessionListener; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.web.servlet.ServletListenerRegistrationBean; import org.springframework.context.annotation.Bean; @@ -74,8 +73,11 @@ @Order(SecurityProperties.BASIC_AUTH_ORDER - 10) public class WebSecurityConfig { - @Autowired - private UserDetailsService userDetailsService; + private final UserDetailsService userDetailsService; + + public WebSecurityConfig(UserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) @@ -98,69 +100,81 @@ public SecurityFilterChain filterChain(HttpSecurity http, auth -> auth // Static assets served by WebConfig resource handlers: without this the // login page cannot load its own scripts, styles or translations. - .requestMatchers("/pages/resources/**", "/portal/javascript/**", - "/portal/themes/**", "/portal/translate/**", "/vle/**", + .requestMatchers( + "/pages/resources/**", + "/portal/javascript/**", + "/portal/themes/**", + "/portal/translate/**", + "/vle/**", "/projectIcons/**") .permitAll() - .requestMatchers("/admin/account/**", "/admin/portal/**", "/admin/news/**", - "/admin/mergeProjectMetadata", "/admin/project/updatesharedprojects", - "/admin/run/replacebase64withpng.html", "/api/admin/**") + .requestMatchers( + "/admin/account/**", + "/admin/portal/**", + "/admin/news/**", + "/admin/mergeProjectMetadata", + "/admin/project/updatesharedprojects", + "/admin/run/replacebase64withpng.html", + "/api/admin/**") .hasRole("ADMINISTRATOR") - .requestMatchers("/api/project/library", "/api/project/community", - "/curriculum/**", "/api/config/preview/**", "/api/user/info", "/api/c-rater/**") + .requestMatchers( + "/api/project/library", + "/api/project/community", + "/curriculum/**", + "/api/config/preview/**", + "/api/user/info", + "/api/c-rater/**") .permitAll() - .requestMatchers(new AntPathRequestMatcher("/api/login/impersonate")) - .hasAnyRole("ADMINISTRATOR", "RESEARCHER") - .requestMatchers(new AntPathRequestMatcher("/admin/**")) + .requestMatchers( + "/api/login/impersonate", + "/admin/**") .hasAnyRole("ADMINISTRATOR", "RESEARCHER") - .requestMatchers(new AntPathRequestMatcher("/author/**")) - .hasAnyRole("TEACHER") - .requestMatchers(new AntPathRequestMatcher("/project/notifyAuthor*/**")) - .hasAnyRole("TEACHER") - .requestMatchers(new AntPathRequestMatcher("/student/account/info")) + .requestMatchers( + "/author/**", + "/project/notifyAuthor*/**", + "/student/account/info") .hasAnyRole("TEACHER") - .requestMatchers(new AntPathRequestMatcher("/student/**")) + .requestMatchers("/student/**") .hasAnyRole("STUDENT") - .requestMatchers(new AntPathRequestMatcher("/studentStatus")) + .requestMatchers("/studentStatus") .hasAnyRole("TEACHER", "STUDENT") - .requestMatchers(new AntPathRequestMatcher("/oauth2/**")).permitAll() - .requestMatchers(new AntPathRequestMatcher("/login/oauth2/**")).permitAll() + .requestMatchers("/oauth2/**", "/login/oauth2/**") + .permitAll() // Password recovery must precede the /api/teacher/** role rule: // without this a teacher who forgot their password needs the TEACHER // role to start recovery. - .requestMatchers(new AntPathRequestMatcher("/api/student/forgot/**")) - .permitAll() - .requestMatchers(new AntPathRequestMatcher("/api/teacher/forgot/**")) - .permitAll() - .requestMatchers(new AntPathRequestMatcher("/api/teacher/register")).permitAll() - .requestMatchers(new AntPathRequestMatcher("/api/student/register")).permitAll() - .requestMatchers(new AntPathRequestMatcher("/api/student/register/questions")) + .requestMatchers( + "/api/student/forgot/**", + "/api/teacher/forgot/**", + "/api/teacher/register", + "/api/student/register", + "/api/student/register/questions", + "/api/*/register/**") .permitAll() - .requestMatchers(new AntPathRequestMatcher("/api/*/register/**")).permitAll() - .requestMatchers(new AntPathRequestMatcher("/api/teacher/**")) + .requestMatchers("/api/teacher/**") .hasAnyRole("TEACHER") - .requestMatchers(new AntPathRequestMatcher("/sso/discourse")) + .requestMatchers("/sso/discourse") .hasAnyRole("TEACHER", "STUDENT") - .requestMatchers(new AntPathRequestMatcher("/api/user/tags")) + .requestMatchers( + "/api/user/tags", + "/api/user/tag/**") .hasAnyRole("TEACHER") - .requestMatchers(new AntPathRequestMatcher("/api/user/tag/**")) - .hasAnyRole("TEACHER") - .requestMatchers(new AntPathRequestMatcher("/api/user/config")).permitAll() - .requestMatchers(new AntPathRequestMatcher("/api/contact")).permitAll() - .requestMatchers(new AntPathRequestMatcher("/api/news/**")).permitAll() - .requestMatchers(new AntPathRequestMatcher("/api/announcement")).permitAll() - .requestMatchers(new AntPathRequestMatcher("/api/project/info/*")).permitAll() - .requestMatchers(new AntPathRequestMatcher("/api/google-user/check-user-exists")) - .permitAll() - .requestMatchers(new AntPathRequestMatcher("/api/google-user/check-user-matches")) + .requestMatchers( + "/api/user/config", + "/api/contact", + "/api/news/**", + "/api/announcement", + "/api/project/info/*", + "/api/google-user/check-user-exists", + "/api/google-user/check-user-matches", + "/previewproject.html", + "/run-survey/**", + "/error", + "/errors/**", + "/favicon.ico", + "/login", + "/") .permitAll() - .requestMatchers(new AntPathRequestMatcher("/previewproject.html")).permitAll() - .requestMatchers(new AntPathRequestMatcher("/run-survey/**")).permitAll() - .requestMatchers(new AntPathRequestMatcher("/error")).permitAll() - .requestMatchers(new AntPathRequestMatcher("/errors/**")).permitAll() - .requestMatchers(new AntPathRequestMatcher("/favicon.ico")).permitAll() - .requestMatchers(new AntPathRequestMatcher("/login")).permitAll() - .requestMatchers(new AntPathRequestMatcher("/")).permitAll() .anyRequest().authenticated()) .formLogin(form -> form.loginPage("/login").permitAll()) .oauth2Login(oauth2 -> oauth2.loginPage("/login") diff --git a/src/test/java/org/wise/portal/spring/impl/WebSecurityConfigAuthorizationTest.java b/src/test/java/org/wise/portal/spring/impl/WebSecurityConfigAuthorizationTest.java index cd230e623..35be94a2e 100644 --- a/src/test/java/org/wise/portal/spring/impl/WebSecurityConfigAuthorizationTest.java +++ b/src/test/java/org/wise/portal/spring/impl/WebSecurityConfigAuthorizationTest.java @@ -100,6 +100,20 @@ public void researcher_nonAccountAdminEndpoint_shouldStayAuthorized() throws Exc assertAuthorized(get("/admin/run/stats").with(user("researcher").roles("RESEARCHER"))); } + @Test + public void administratorAndResearcher_impersonate_shouldBeAuthorized() throws Exception { + assertAuthorized(get("/api/login/impersonate").with(user("admin").roles("ADMINISTRATOR"))); + assertAuthorized(get("/api/login/impersonate").with(user("researcher").roles("RESEARCHER"))); + } + + @Test + public void teacherAndStudent_impersonate_shouldBeForbidden() throws Exception { + mockMvc.perform(get("/api/login/impersonate").with(user("teacher").roles("TEACHER"))) + .andExpect(status().isForbidden()); + mockMvc.perform(get("/api/login/impersonate").with(user("student").roles("STUDENT"))) + .andExpect(status().isForbidden()); + } + @Test public void unauthenticated_projectLibraryAndPreviewEndpoints_shouldBeAllowed() throws Exception { assertAuthorized(get("/api/project/library")); @@ -180,6 +194,7 @@ public void unauthenticated_protectedEndpoints_shouldBeDenied() throws Exception assertDeniedForAnonymous(get("/api/teacher/profile")); assertDeniedForAnonymous(get("/author/authorproject.html")); assertDeniedForAnonymous(get("/api/admin/config")); + assertDeniedForAnonymous(get("/api/login/impersonate")); } /**