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());
+    }
+}