diff --git a/analytics-service/pom.xml b/analytics-service/pom.xml index f0e170f388015f2e23bd67a31b96155ad297053f..295c001d3ddba4530183f71c3af1d2d10bf0a343 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 9fcdad79d486f5239e1b12d2af2d9ad4cf7484d6..2853bb138d5f446c7312a2ea7e1cb3ea9bc3253a 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 1e2865e158eafe88bddde3bf7f24262d6a841e7b..fed8c9964c0ddfc098adc7e09284729e967cfce6 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 e2b74da9c97acbaed451b8080b7582316644d0e1..55057431f83b391aae8b8b8bc81da02e0530f07a 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 a223b56dd2a2dc88c69595092fbdf428b999a012..66a5fba51eb17f0180fbbb3696b7eef2ff5b84fc 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