From 0930ca46043b9e18ecd20ebea86fb626a89ef3e3 Mon Sep 17 00:00:00 2001
From: Vilem Gottwald <xvigo.dev@gmail.com>
Date: Wed, 8 May 2024 01:20:54 +0200
Subject: [PATCH] analytics service security

---
 analytics-service/pom.xml                     | 11 ++++-
 .../cz/muni/fi/obs/AnalyticsManagement.java   | 34 ++++++++++++++
 .../obs/controller/AnalyticsController.java   | 42 ++++++++++++++++--
 .../src/main/resources/application.yml        |  9 +++-
 .../controller/AnalyticsControllerTest.java   | 44 +++++++++++++------
 5 files changed, 119 insertions(+), 21 deletions(-)

diff --git a/analytics-service/pom.xml b/analytics-service/pom.xml
index f0e170f..295c001 100644
--- a/analytics-service/pom.xml
+++ b/analytics-service/pom.xml
@@ -47,6 +47,10 @@
             <groupId>org.springframework.data</groupId>
             <artifactId>spring-data-commons</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
+        </dependency>
         <dependency>
             <groupId>org.projectlombok</groupId>
             <artifactId>lombok</artifactId>
@@ -108,8 +112,11 @@
             <artifactId>rest-assured</artifactId>
             <scope>test</scope>
         </dependency>
-
-
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-test</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/analytics-service/src/main/java/cz/muni/fi/obs/AnalyticsManagement.java b/analytics-service/src/main/java/cz/muni/fi/obs/AnalyticsManagement.java
index 9fcdad7..2853bb1 100644
--- a/analytics-service/src/main/java/cz/muni/fi/obs/AnalyticsManagement.java
+++ b/analytics-service/src/main/java/cz/muni/fi/obs/AnalyticsManagement.java
@@ -1,9 +1,16 @@
 package cz.muni.fi.obs;
 
+import io.swagger.v3.oas.models.security.SecurityScheme;
+import org.springdoc.core.customizers.OpenApiCustomizer;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.cloud.openfeign.EnableFeignClients;
+import org.springframework.context.annotation.Bean;
+import org.springframework.http.HttpMethod;
 import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.web.SecurityFilterChain;
 import org.springframework.transaction.annotation.EnableTransactionManagement;
 
 @SpringBootApplication
@@ -12,7 +19,34 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
 @EnableScheduling
 public class AnalyticsManagement {
 
+    public static final String SECURITY_SCHEME_BEARER = "Bearer";
+
+
     public static void main(String[] args) {
         SpringApplication.run(AnalyticsManagement.class, args);
     }
+
+
+    @Bean
+    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+        http
+                .authorizeHttpRequests(x -> x
+                        .requestMatchers(HttpMethod.GET, "/api/**").hasAuthority("SCOPE_test_read")
+                        .anyRequest().permitAll()
+                )
+                .oauth2ResourceServer(oauth2 -> oauth2.opaqueToken(Customizer.withDefaults()))
+        ;
+        return http.build();
+    }
+
+    @Bean
+    public OpenApiCustomizer openAPICustomizer() {
+        return openApi -> openApi.getComponents()
+                                 .addSecuritySchemes(SECURITY_SCHEME_BEARER,
+                                                     new SecurityScheme()
+                                                             .type(SecurityScheme.Type.HTTP)
+                                                             .scheme("bearer")
+                                                             .description("Provide an access token")
+                                 );
+    }
 }
diff --git a/analytics-service/src/main/java/cz/muni/fi/obs/controller/AnalyticsController.java b/analytics-service/src/main/java/cz/muni/fi/obs/controller/AnalyticsController.java
index 1e2865e..fed8c99 100644
--- a/analytics-service/src/main/java/cz/muni/fi/obs/controller/AnalyticsController.java
+++ b/analytics-service/src/main/java/cz/muni/fi/obs/controller/AnalyticsController.java
@@ -1,10 +1,15 @@
 package cz.muni.fi.obs.controller;
 
+import cz.muni.fi.obs.AnalyticsManagement;
 import cz.muni.fi.obs.api.DailySummaryRequest;
 import cz.muni.fi.obs.api.DailySummaryResult;
 import cz.muni.fi.obs.api.MonthlySummaryRequest;
 import cz.muni.fi.obs.api.MonthlySummaryResult;
 import cz.muni.fi.obs.facade.AnalyticsFacade;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
 import jakarta.validation.Valid;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -22,16 +27,45 @@ public class AnalyticsController {
         this.analyticsFacade = analyticsFacade;
     }
 
+    @Operation(
+            summary = "Get daily summary for account",
+            security = @SecurityRequirement(name = AnalyticsManagement.SECURITY_SCHEME_BEARER,
+                                            scopes = {"SCOPE_test_read"}),
+            responses = {
+                    @ApiResponse(responseCode = "200", description = "Summary retrieved"),
+                    @ApiResponse(responseCode = "401", description = "Unauthorized", content = @Content()),
+                    @ApiResponse(responseCode = "403", description = "Forbidden", content = @Content()),
+            }
+    )
     @PostMapping("/daily-summary")
-    public ResponseEntity<DailySummaryResult> getDailySummary(@PathVariable String accountNumber, @Valid @RequestBody DailySummaryRequest request) {
-        log.info("Received request for daily summary for account number: {}, year: {}, month: {}", accountNumber, request.year(), request.month());
+    public ResponseEntity<DailySummaryResult> getDailySummary(@PathVariable String accountNumber,
+                                                              @Valid @RequestBody DailySummaryRequest request) {
+        log.info("Received request for daily summary for account number: {}, year: {}, month: {}",
+                 accountNumber,
+                 request.year(),
+                 request.month()
+        );
         DailySummaryResult result = analyticsFacade.getDailySummary(accountNumber, request.year(), request.month());
         return ResponseEntity.ok(result);
     }
 
+    @Operation(
+            summary = "Get monthly summary for account",
+            security = @SecurityRequirement(name = AnalyticsManagement.SECURITY_SCHEME_BEARER,
+                                            scopes = {"SCOPE_test_read"}),
+            responses = {
+                    @ApiResponse(responseCode = "200", description = "Summary retrieved"),
+                    @ApiResponse(responseCode = "401", description = "Unauthorized", content = @Content()),
+                    @ApiResponse(responseCode = "403", description = "Forbidden", content = @Content()),
+            }
+    )
     @PostMapping("/monthly-summary")
-    public ResponseEntity<MonthlySummaryResult> getMonthlySummary(@PathVariable String accountNumber, @Valid @RequestBody MonthlySummaryRequest request) {
-        log.info("Received request for monthly summary for account number: {}, year: {}", accountNumber, request.year());
+    public ResponseEntity<MonthlySummaryResult> getMonthlySummary(@PathVariable String accountNumber,
+                                                                  @Valid @RequestBody MonthlySummaryRequest request) {
+        log.info("Received request for monthly summary for account number: {}, year: {}",
+                 accountNumber,
+                 request.year()
+        );
         MonthlySummaryResult result = analyticsFacade.getMonthlySummary(accountNumber, request.year(), request.month());
         return ResponseEntity.ok(result);
     }
diff --git a/analytics-service/src/main/resources/application.yml b/analytics-service/src/main/resources/application.yml
index e2b74da..5505743 100644
--- a/analytics-service/src/main/resources/application.yml
+++ b/analytics-service/src/main/resources/application.yml
@@ -27,7 +27,14 @@ spring:
       enabled: false
     jdbc:
       initialize-schema: always
-
+  security:
+    oauth2:
+      resourceserver:
+        opaque-token:
+          introspection-uri: https://oidc.muni.cz/oidc/introspect
+          # Martin Kuba's testing resource server
+          client-id: d57b3a8f-156e-46de-9f27-39c4daee05e1
+          client-secret: fa228ebc-4d54-4cda-901e-4d6287f8b1652a9c9c44-73c9-4502-973f-bcdb4a8ec96a
 etl:
   transaction-service:
     url: 'http://host.docker.internal:8082/api/transaction-service'
\ No newline at end of file
diff --git a/analytics-service/src/test/java/cz/muni/fi/obs/unit/controller/AnalyticsControllerTest.java b/analytics-service/src/test/java/cz/muni/fi/obs/unit/controller/AnalyticsControllerTest.java
index a223b56..66a5fba 100644
--- a/analytics-service/src/test/java/cz/muni/fi/obs/unit/controller/AnalyticsControllerTest.java
+++ b/analytics-service/src/test/java/cz/muni/fi/obs/unit/controller/AnalyticsControllerTest.java
@@ -8,15 +8,19 @@ import cz.muni.fi.obs.api.MonthlySummaryRequest;
 import cz.muni.fi.obs.api.MonthlySummaryResult;
 import cz.muni.fi.obs.controller.AnalyticsController;
 import cz.muni.fi.obs.facade.AnalyticsFacade;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.runner.RunWith;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
 import org.springframework.boot.test.mock.mockito.MockBean;
 import org.springframework.http.MediaType;
+import org.springframework.security.test.context.support.WithMockUser;
 import org.springframework.test.context.ContextConfiguration;
 import org.springframework.test.context.junit4.SpringRunner;
 import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
 
 import java.time.LocalDate;
 import java.util.ArrayList;
@@ -24,6 +28,7 @@ import java.util.ArrayList;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
+import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 
@@ -32,60 +37,71 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
 @ContextConfiguration(classes = {Application.class})
 class AnalyticsControllerTest {
 
+    private final ObjectMapper objectMapper = new ObjectMapper();
     @MockBean
     private AnalyticsFacade analyticsFacade;
-
     @Autowired
+    private WebApplicationContext context;
     private MockMvc mockMvc;
 
-    private final ObjectMapper objectMapper = new ObjectMapper();
+    @BeforeEach
+    public void setup() {
+        mockMvc = MockMvcBuilders
+                .webAppContextSetup(context)
+                .apply(springSecurity())
+                .build();
+    }
 
+    @WithMockUser(username = "test", authorities = {"SCOPE_test_read"})
     @Test
     void getDailySummary_validRequest_returnsASummary() throws Exception {
         when(analyticsFacade.getDailySummary(any(String.class), any(Integer.class), any(Integer.class)))
                 .thenReturn(new DailySummaryResult(LocalDate.now(), new ArrayList<>()));
 
         mockMvc.perform(post("/v1/12345/daily-summary")
-                        .contentType(MediaType.APPLICATION_JSON)
-                        .content(objectMapper.writeValueAsString(new DailySummaryRequest(2021, 1))))
-                .andExpect(status().isOk());
+                                .contentType(MediaType.APPLICATION_JSON)
+                                .content(objectMapper.writeValueAsString(new DailySummaryRequest(2021, 1))))
+               .andExpect(status().isOk());
 
         verify(analyticsFacade).getDailySummary("12345", 2021, 1);
     }
 
+    @WithMockUser(username = "test", authorities = {"SCOPE_test_read"})
     @Test
     void getDailySummary_badRequest_throwsException() throws Exception {
         when(analyticsFacade.getDailySummary(any(String.class), any(Integer.class), any(Integer.class)))
                 .thenReturn(new DailySummaryResult(LocalDate.now(), new ArrayList<>()));
 
         mockMvc.perform(post("/v1/12345/daily-summary")
-                        .contentType(MediaType.APPLICATION_JSON)
-                        .content(objectMapper.writeValueAsString(new DailySummaryRequest(2021, -1))))
-                .andExpect(status().isBadRequest());
+                                .contentType(MediaType.APPLICATION_JSON)
+                                .content(objectMapper.writeValueAsString(new DailySummaryRequest(2021, -1))))
+               .andExpect(status().isBadRequest());
 
     }
 
+    @WithMockUser(username = "test", authorities = {"SCOPE_test_read"})
     @Test
     void getMonthlySummary_validRequest_returnsASummary() throws Exception {
         when(analyticsFacade.getMonthlySummary(any(String.class), any(Integer.class), any(Integer.class)))
                 .thenReturn(new MonthlySummaryResult(LocalDate.now(), null));
 
         mockMvc.perform(post("/v1/12345/monthly-summary")
-                        .contentType(MediaType.APPLICATION_JSON)
-                        .content(objectMapper.writeValueAsString(new MonthlySummaryRequest(2021, 1))))
-                .andExpect(status().isOk());
+                                .contentType(MediaType.APPLICATION_JSON)
+                                .content(objectMapper.writeValueAsString(new MonthlySummaryRequest(2021, 1))))
+               .andExpect(status().isOk());
 
         verify(analyticsFacade).getMonthlySummary("12345", 2021, 1);
     }
 
+    @WithMockUser(username = "test", authorities = {"SCOPE_test_read"})
     @Test
     void getMonthlySummary_badRequest_throwsException() throws Exception {
         when(analyticsFacade.getMonthlySummary(any(String.class), any(Integer.class), any(Integer.class)))
                 .thenReturn(new MonthlySummaryResult(LocalDate.now(), null));
 
         mockMvc.perform(post("/v1/12345/monthly-summary")
-                        .contentType(MediaType.APPLICATION_JSON)
-                        .content(objectMapper.writeValueAsString(new MonthlySummaryRequest(-10, 1))))
-                .andExpect(status().isBadRequest());
+                                .contentType(MediaType.APPLICATION_JSON)
+                                .content(objectMapper.writeValueAsString(new MonthlySummaryRequest(-10, 1))))
+               .andExpect(status().isBadRequest());
     }
 }
\ No newline at end of file
-- 
GitLab