From 525d33ed01ee4edea635f200eb0b3f9eb1b431bb Mon Sep 17 00:00:00 2001 From: Andreas Svanberg <andreass@dsv.su.se> Date: Thu, 12 Sep 2024 17:40:50 +0200 Subject: [PATCH] Protected admin section --- .gitignore | 1 + pom.xml | 17 +++++++ .../se/su/dsv/oauth2/AuthorizationServer.java | 7 ++- .../su/dsv/oauth2/shibboleth/Entitlement.java | 4 ++ .../se/su/dsv/oauth2/web/AdminController.java | 14 ++++++ .../se/su/dsv/oauth2/web/ErrorController.java | 12 +++++ .../su/dsv/oauth2/web/PublicController.java | 15 ++++++ src/main/resources/application.yml | 4 ++ src/main/resources/templates/admin/index.jte | 1 + .../resources/templates/error/forbidden.jte | 1 + src/main/resources/templates/index.jte | 17 +++++++ .../dsv/oauth2/web/AdminControllerTest.java | 47 +++++++++++++++++++ 12 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 src/main/java/se/su/dsv/oauth2/web/AdminController.java create mode 100644 src/main/java/se/su/dsv/oauth2/web/ErrorController.java create mode 100644 src/main/java/se/su/dsv/oauth2/web/PublicController.java create mode 100644 src/main/resources/templates/admin/index.jte create mode 100644 src/main/resources/templates/error/forbidden.jte create mode 100644 src/main/resources/templates/index.jte create mode 100644 src/test/java/se/su/dsv/oauth2/web/AdminControllerTest.java diff --git a/.gitignore b/.gitignore index 7c27274..4fb7c2b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ target/ !**/src/main/**/target/ !**/src/test/**/target/ src/main/resources/application-local.yml +jte-classes/ ### STS ### .apt_generated diff --git a/pom.xml b/pom.xml index 59a46b5..df01d31 100644 --- a/pom.xml +++ b/pom.xml @@ -34,6 +34,18 @@ <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> + + <dependency> + <groupId>gg.jte</groupId> + <artifactId>jte-spring-boot-starter-3</artifactId> + <version>3.1.12</version> + </dependency> + <dependency> + <groupId>gg.jte</groupId> + <artifactId>jte</artifactId> + <version>3.1.12</version> + </dependency> + <dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-core</artifactId> @@ -73,6 +85,11 @@ <artifactId>spring-boot-testcontainers</artifactId> <scope>test</scope> </dependency> + <dependency> + <groupId>org.springframework.security</groupId> + <artifactId>spring-security-test</artifactId> + <scope>test</scope> + </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>junit-jupiter</artifactId> diff --git a/src/main/java/se/su/dsv/oauth2/AuthorizationServer.java b/src/main/java/se/su/dsv/oauth2/AuthorizationServer.java index 4995d6b..7aa4054 100644 --- a/src/main/java/se/su/dsv/oauth2/AuthorizationServer.java +++ b/src/main/java/se/su/dsv/oauth2/AuthorizationServer.java @@ -25,6 +25,7 @@ import org.springframework.security.web.access.intercept.AuthorizationFilter; import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; import org.springframework.security.web.authentication.preauth.j2ee.J2eePreAuthenticatedProcessingFilter; +import se.su.dsv.oauth2.shibboleth.Entitlement; import se.su.dsv.oauth2.shibboleth.ShibbolethAuthenticationDetailsSource; import java.util.UUID; @@ -84,14 +85,18 @@ public class AuthorizationServer extends SpringBootServletInitializer { */ @Bean @Order(2) - public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) + public SecurityFilterChain defaultSecurityFilterChain( + HttpSecurity http, + Config config) throws Exception { http.authorizeHttpRequests(authorize -> authorize + .requestMatchers("/admin/**").hasAuthority(Entitlement.asAuthority(config.adminEntitlement())) .anyRequest().authenticated()); http.exceptionHandling(exceptions -> exceptions + .accessDeniedPage("/forbidden") .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))); http.jee(jee -> jee diff --git a/src/main/java/se/su/dsv/oauth2/shibboleth/Entitlement.java b/src/main/java/se/su/dsv/oauth2/shibboleth/Entitlement.java index cda17d6..bf3406e 100644 --- a/src/main/java/se/su/dsv/oauth2/shibboleth/Entitlement.java +++ b/src/main/java/se/su/dsv/oauth2/shibboleth/Entitlement.java @@ -5,6 +5,10 @@ import org.springframework.security.core.GrantedAuthority; public record Entitlement(String entitlement) implements GrantedAuthority { @Override public String getAuthority() { + return asAuthority(entitlement); + } + + public static String asAuthority(String entitlement) { return "ENTITLEMENT_" + entitlement; } } diff --git a/src/main/java/se/su/dsv/oauth2/web/AdminController.java b/src/main/java/se/su/dsv/oauth2/web/AdminController.java new file mode 100644 index 0000000..49c576a --- /dev/null +++ b/src/main/java/se/su/dsv/oauth2/web/AdminController.java @@ -0,0 +1,14 @@ +package se.su.dsv.oauth2.web; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +@RequestMapping("/admin") +public class AdminController { + @GetMapping + public String index() { + return "admin/index"; + } +} diff --git a/src/main/java/se/su/dsv/oauth2/web/ErrorController.java b/src/main/java/se/su/dsv/oauth2/web/ErrorController.java new file mode 100644 index 0000000..ee23041 --- /dev/null +++ b/src/main/java/se/su/dsv/oauth2/web/ErrorController.java @@ -0,0 +1,12 @@ +package se.su.dsv.oauth2.web; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class ErrorController { + @GetMapping("/forbidden") + public String forbidden() { + return "error/forbidden"; + } +} diff --git a/src/main/java/se/su/dsv/oauth2/web/PublicController.java b/src/main/java/se/su/dsv/oauth2/web/PublicController.java new file mode 100644 index 0000000..2a4eb29 --- /dev/null +++ b/src/main/java/se/su/dsv/oauth2/web/PublicController.java @@ -0,0 +1,15 @@ +package se.su.dsv.oauth2.web; + +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class PublicController { + @GetMapping("/") + public String index(Model model, Authentication authentication) { + model.addAttribute("displayName", authentication.getName()); + return "index"; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c6d0a17..f7a1b42 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -3,3 +3,7 @@ spring: name: dsv-oauth2-authorization-server profiles: include: local +gg: + jte: + templateLocation: src/main/resources/templates + developmentMode: true diff --git a/src/main/resources/templates/admin/index.jte b/src/main/resources/templates/admin/index.jte new file mode 100644 index 0000000..e2a6e18 --- /dev/null +++ b/src/main/resources/templates/admin/index.jte @@ -0,0 +1 @@ +admin/index.jte diff --git a/src/main/resources/templates/error/forbidden.jte b/src/main/resources/templates/error/forbidden.jte new file mode 100644 index 0000000..57deb3a --- /dev/null +++ b/src/main/resources/templates/error/forbidden.jte @@ -0,0 +1 @@ +Nuh uh \ No newline at end of file diff --git a/src/main/resources/templates/index.jte b/src/main/resources/templates/index.jte new file mode 100644 index 0000000..cdbeba0 --- /dev/null +++ b/src/main/resources/templates/index.jte @@ -0,0 +1,17 @@ +@param String displayName +@param org.springframework.web.servlet.support.RequestContext springMacroRequestContext + +<!DOCTYPE html> +<html lang="en"> +<head> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>DSV OAuth 2.0</title> +</head> +<body> +<main> + <h1>DSV OAuth 2.0</h1> + <p>Welcome ${displayName} to DSV's OAuth 2.0 information page</p> + <a href="${springMacroRequestContext.getContextUrl("/admin")}">Admin</a> +</main> +</body> +</html> diff --git a/src/test/java/se/su/dsv/oauth2/web/AdminControllerTest.java b/src/test/java/se/su/dsv/oauth2/web/AdminControllerTest.java new file mode 100644 index 0000000..179a837 --- /dev/null +++ b/src/test/java/se/su/dsv/oauth2/web/AdminControllerTest.java @@ -0,0 +1,47 @@ +package se.su.dsv.oauth2.web; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.test.web.servlet.MockMvc; +import org.testcontainers.containers.MariaDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import se.su.dsv.oauth2.shibboleth.Entitlement; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest( + properties = { + "se.su.dsv.oauth2.admin-entitlement=" + AdminControllerTest.ADMIN_ENTITLEMENT + } +) +@Testcontainers +@AutoConfigureMockMvc +class AdminControllerTest { + static final String ADMIN_ENTITLEMENT = "ADMIN"; + + @Container + @ServiceConnection + static MariaDBContainer<?> mariaDBContainer = new MariaDBContainer<>("mariadb:10.11"); + + @Autowired + MockMvc mockMvc; + + @Test + void is_protected() throws Exception { + mockMvc.perform(get("/admin")) + .andExpect(redirectedUrl("http://localhost/login")); + } + + @Test + void is_accessible_with_admin_authority() throws Exception { + mockMvc.perform(get("/admin") + .with(user("admin").authorities(new Entitlement(ADMIN_ENTITLEMENT)))) + .andExpect(status().isOk()); + } +}