diff --git a/dbrepo-metadata-service/pom.xml b/dbrepo-metadata-service/pom.xml index 8cd08e853640a0b6c810c36b65a5c46d4cc425c0..c76bc5517788001521cf01d551573d0db6b044f5 100644 --- a/dbrepo-metadata-service/pom.xml +++ b/dbrepo-metadata-service/pom.xml @@ -151,10 +151,20 @@ <version>${c3p0-hibernate.version}</version> </dependency> <!-- Monitoring --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-aop</artifactId> + </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> - <scope>runtime</scope> + <version>${micrometer.version}</version> + </dependency> + <dependency> + <groupId>io.micrometer</groupId> + <artifactId>micrometer-observation-test</artifactId> + <version>${micrometer.version}</version> + <scope>test</scope> </dependency> <!-- Authentication --> <dependency> diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java index cd0bc94d5865a493538444a88a9bbb74333c30ab..d3c26da5659dd1f6c84141a74854c76972c69c79 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java @@ -10,6 +10,7 @@ import at.tuwien.mapper.DatabaseMapper; import at.tuwien.service.AccessService; import at.tuwien.utils.PrincipalUtil; import at.tuwien.utils.UserUtil; +import io.micrometer.observation.annotation.Observed; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -46,6 +47,7 @@ public class AccessEndpoint { @PostMapping("/{userId}") @Transactional + @Observed(name = "dbr_access_give") @PreAuthorize("hasAuthority('create-database-access')") @Operation(summary = "Give access to some database", security = @SecurityRequirement(name = "bearerAuth")) @ApiResponses(value = { @@ -89,6 +91,7 @@ public class AccessEndpoint { @PutMapping("/{userId}") @Transactional + @Observed(name = "dbr_access_modify") @PreAuthorize("hasAuthority('update-database-access')") @Operation(summary = "Modify access to some database", security = @SecurityRequirement(name = "bearerAuth")) @ApiResponses(value = { @@ -126,6 +129,7 @@ public class AccessEndpoint { @GetMapping @Transactional + @Observed(name = "dbr_access_check") @PreAuthorize("hasAuthority('check-database-access')") @Operation(summary = "Check access to some database", security = @SecurityRequirement(name = "bearerAuth")) @ApiResponses(value = { @@ -157,6 +161,7 @@ public class AccessEndpoint { @DeleteMapping("/{userId}") @Transactional + @Observed(name = "dbr_access_delete") @PreAuthorize("hasAuthority('delete-database-access')") @Operation(summary = "Revoke access to some database", security = @SecurityRequirement(name = "bearerAuth")) @ApiResponses(value = { diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/validation/EndpointValidator.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/validation/EndpointValidator.java index 6169e677d4ae1b2b3120262bc91235cb3c084380..3176f6327d630e022e1baa489effc539b61119d2 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/validation/EndpointValidator.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/validation/EndpointValidator.java @@ -92,17 +92,37 @@ public class EndpointValidator { .filter(c -> needSize.contains(c.getType())) .findFirst(); if (optional0.isPresent()) { + log.error("Validation failed: column {} needs size parameter", optional0.get().getName() ); throw new TableMalformedException("Validation failed: column " + optional0.get().getName() + " needs size parameter"); } /* check size and d */ final Optional<ColumnCreateDto> optional1 = data.getColumns() .stream() - .filter(c -> Objects.isNull(c.getSize()) || Objects.isNull(c.getD())) .filter(c -> needSizeAndD.contains(c.getType())) + .filter(c -> Objects.isNull(c.getSize()) || Objects.isNull(c.getD())) .findFirst(); if (optional1.isPresent()) { + log.error("Validation failed: column {} needs size and d parameter", optional1.get().getName()); throw new TableMalformedException("Validation failed: column " + optional1.get().getName() + " needs size and d parameter"); } + final Optional<ColumnCreateDto> optional1a = data.getColumns() + .stream() + .filter(c -> needSizeAndD.contains(c.getType())) + .filter(c -> c.getSize() > 65 || c.getD() > 38) + .findFirst(); + if (optional1a.isPresent()) { + log.error("Validation failed: column {} needs size (max 65) and d (max 30)", optional1a.get().getName()); + throw new TableMalformedException("Validation failed: column " + optional1a.get().getName() + " needs size (max 65) and d (max 30)"); + } + final Optional<ColumnCreateDto> optional1b = data.getColumns() + .stream() + .filter(c -> needSizeAndD.contains(c.getType())) + .filter(c -> c.getSize() < c.getD()) + .findFirst(); + if (optional1b.isPresent()) { + log.error("Validation failed: column {} needs size >= d", optional1b.get().getName()); + throw new TableMalformedException("Validation failed: column " + optional1b.get().getName() + " needs size >= d"); + } /* check enum */ final Optional<ColumnCreateDto> optional2 = data.getColumns() .stream() @@ -110,6 +130,7 @@ public class EndpointValidator { .filter(c -> c.getEnums() == null || c.getEnums().isEmpty()) .findFirst(); if (optional2.isPresent()) { + log.error("Validation failed: column {} needs at least 1 allowed enum value", optional2.get().getName()); throw new TableMalformedException("Validation failed: column " + optional2.get().getName() + " needs at least 1 allowed enum value"); } /* check set */ @@ -119,6 +140,7 @@ public class EndpointValidator { .filter(c -> c.getEnums() == null || c.getSets().isEmpty()) .findFirst(); if (optional3.isPresent()) { + log.error("Validation failed: column {} needs at least 1 allowed set value", optional3.get().getName()); throw new TableMalformedException("Validation failed: column " + optional3.get().getName() + " needs at least 1 allowed set value"); } /* check date */ @@ -128,6 +150,7 @@ public class EndpointValidator { .filter(c -> Objects.isNull(c.getDfid())) .findFirst(); if (optional4.isPresent()) { + log.error("Validation failed: column {} needs a format", optional4.get().getName()); throw new TableMalformedException("Validation failed: column " + optional4.get().getName() + " needs a format"); } } diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/TableEndpointUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/TableEndpointUnitTest.java index 9005b65ff7911f7669d98056a899d36d3fe698fa..5ee6b057a2cb95b3d968e48d6c39fdc9144e8ec7 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/TableEndpointUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/TableEndpointUnitTest.java @@ -6,6 +6,8 @@ import at.tuwien.annotations.MockOpensearch; import at.tuwien.api.database.table.TableBriefDto; import at.tuwien.api.database.table.TableCreateDto; import at.tuwien.api.database.table.TableDto; +import at.tuwien.api.database.table.columns.ColumnCreateDto; +import at.tuwien.api.database.table.columns.ColumnTypeDto; import at.tuwien.entities.database.Database; import at.tuwien.entities.database.DatabaseAccess; import at.tuwien.entities.database.table.Table; @@ -155,6 +157,109 @@ public class TableEndpointUnitTest extends BaseUnitTest { }); } + @Test + @WithMockUser(username = USER_3_USERNAME, authorities = {"create-table"}) + public void create_publicDecimalColumnSizeMissing_fails() { + final TableCreateDto request = TableCreateDto.builder() + .name("Some Table") + .description("Some Description") + .columns(List.of(ColumnCreateDto.builder() + .name("ID") + .type(ColumnTypeDto.DECIMAL) + .build())) + .constraints(null) + .build(); + + /* test */ + assertThrows(TableMalformedException.class, () -> { + generic_create(DATABASE_3_ID, DATABASE_3, request, USER_1_ID, USER_1_PRINCIPAL, DATABASE_3_USER_1_WRITE_OWN_ACCESS); + }); + } + + @Test + @WithMockUser(username = USER_3_USERNAME, authorities = {"create-table"}) + public void create_publicDecimalColumnSizeTooSmall_fails() { + final TableCreateDto request = TableCreateDto.builder() + .name("Some Table") + .description("Some Description") + .columns(List.of(ColumnCreateDto.builder() + .name("ID") + .type(ColumnTypeDto.DECIMAL) + .size(-1) + .d(0) + .build())) + .constraints(null) + .build(); + + /* test */ + assertThrows(TableMalformedException.class, () -> { + generic_create(DATABASE_3_ID, DATABASE_3, request, USER_1_ID, USER_1_PRINCIPAL, DATABASE_3_USER_1_WRITE_OWN_ACCESS); + }); + } + + @Test + @WithMockUser(username = USER_3_USERNAME, authorities = {"create-table"}) + public void create_publicDecimalColumnSizeTooBig_fails() { + final TableCreateDto request = TableCreateDto.builder() + .name("Some Table") + .description("Some Description") + .columns(List.of(ColumnCreateDto.builder() + .name("ID") + .type(ColumnTypeDto.DECIMAL) + .size(66) + .d(0) + .build())) + .constraints(null) + .build(); + + /* test */ + assertThrows(TableMalformedException.class, () -> { + generic_create(DATABASE_3_ID, DATABASE_3, request, USER_1_ID, USER_1_PRINCIPAL, DATABASE_3_USER_1_WRITE_OWN_ACCESS); + }); + } + + @Test + @WithMockUser(username = USER_3_USERNAME, authorities = {"create-table"}) + public void create_publicDecimalColumnDTooBig_fails() { + final TableCreateDto request = TableCreateDto.builder() + .name("Some Table") + .description("Some Description") + .columns(List.of(ColumnCreateDto.builder() + .name("ID") + .type(ColumnTypeDto.DECIMAL) + .size(0) + .d(39) + .build())) + .constraints(null) + .build(); + + /* test */ + assertThrows(TableMalformedException.class, () -> { + generic_create(DATABASE_3_ID, DATABASE_3, request, USER_1_ID, USER_1_PRINCIPAL, DATABASE_3_USER_1_WRITE_OWN_ACCESS); + }); + } + + @Test + @WithMockUser(username = USER_3_USERNAME, authorities = {"create-table"}) + public void create_publicDecimalColumnDBiggerSize_fails() { + final TableCreateDto request = TableCreateDto.builder() + .name("Some Table") + .description("Some Description") + .columns(List.of(ColumnCreateDto.builder() + .name("ID") + .type(ColumnTypeDto.DECIMAL) + .size(9) + .d(10) + .build())) + .constraints(null) + .build(); + + /* test */ + assertThrows(TableMalformedException.class, () -> { + generic_create(DATABASE_3_ID, DATABASE_3, request, USER_1_ID, USER_1_PRINCIPAL, DATABASE_3_USER_1_WRITE_OWN_ACCESS); + }); + } + @Test @WithAnonymousUser public void findById_publicAnonymous_succeeds() throws DatabaseNotFoundException, TableNotFoundException, diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mvc/ActuatorEndpointMvcTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mvc/ActuatorEndpointMvcTest.java new file mode 100644 index 0000000000000000000000000000000000000000..11d52c79efd91d66aec071b20d9b7f409d507dd0 --- /dev/null +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mvc/ActuatorEndpointMvcTest.java @@ -0,0 +1,50 @@ +package at.tuwien.mvc; + +import at.tuwien.BaseUnitTest; +import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockOpensearch; +import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.actuate.observability.AutoConfigureObservability; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Log4j2 +@ExtendWith(SpringExtension.class) +@AutoConfigureMockMvc +@SpringBootTest +@AutoConfigureObservability +@MockAmqp +@MockOpensearch +public class ActuatorEndpointMvcTest extends BaseUnitTest { + + @Autowired + private MockMvc mockMvc; + + @Test + public void actuatorInfo_succeeds() throws Exception { + + /* test */ + this.mockMvc.perform(get("/actuator/info")) + .andDo(print()) + .andExpect(status().isOk()); + } + + @Test + public void actuatorPrometheus_succeeds() throws Exception { + + /* test */ + this.mockMvc.perform(get("/actuator/prometheus")) + .andDo(print()) + .andExpect(status().isOk()); + } + +} diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mvc/PrometheusEndpointMvcTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mvc/PrometheusEndpointMvcTest.java new file mode 100644 index 0000000000000000000000000000000000000000..efbe24aa944480ea5e7b2e715ff3da1c6255e1e8 --- /dev/null +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mvc/PrometheusEndpointMvcTest.java @@ -0,0 +1,108 @@ +package at.tuwien.mvc; + +import at.tuwien.BaseUnitTest; +import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockOpensearch; +import at.tuwien.api.database.AccessTypeDto; +import at.tuwien.api.database.DatabaseGiveAccessDto; +import at.tuwien.api.database.DatabaseModifyAccessDto; +import at.tuwien.config.MetricsConfig; +import at.tuwien.endpoints.AccessEndpoint; +import io.micrometer.observation.tck.TestObservationRegistry; +import lombok.SneakyThrows; +import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.actuate.observability.AutoConfigureObservability; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.List; + +import static io.micrometer.observation.tck.TestObservationRegistryAssert.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Log4j2 +@ExtendWith(SpringExtension.class) +@AutoConfigureMockMvc +@SpringBootTest +@Import(MetricsConfig.class) +@AutoConfigureObservability +@MockAmqp +@MockOpensearch +public class PrometheusEndpointMvcTest extends BaseUnitTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private TestObservationRegistry registry; + + @Autowired + private AccessEndpoint accessEndpoint; + + @TestConfiguration + static class ObservationTestConfiguration { + + @Bean + public TestObservationRegistry observationRegistry() { + return TestObservationRegistry.create(); + } + } + + @Test + public void prometheus_succeeds() throws Exception { + + /* test */ + this.mockMvc.perform(get("/actuator/prometheus")) + .andDo(print()) + .andExpect(status().isOk()); + } + + @Test + @WithMockUser(username = USER_1_USERNAME, authorities = {"create-database-access", "update-database-access", "check-database-access", "delete-database-access"}) + public void prometheusAccessEndpoint_succeeds() throws Exception { + + /* mock */ + try { + accessEndpoint.create(DATABASE_1_ID, USER_1_ID, DatabaseGiveAccessDto.builder().type(AccessTypeDto.READ).build(), USER_1_PRINCIPAL); + } catch (Exception e) { + /* ignore */ + } + try { + accessEndpoint.update(DATABASE_1_ID, USER_1_ID, DatabaseModifyAccessDto.builder().type(AccessTypeDto.READ).build(), USER_1_PRINCIPAL); + } catch (Exception e) { + /* ignore */ + } + try { + accessEndpoint.find(DATABASE_1_ID, USER_1_PRINCIPAL); + } catch (Exception e) { + /* ignore */ + } + try { + accessEndpoint.revoke(DATABASE_1_ID, USER_1_ID, USER_1_PRINCIPAL); + } catch (Exception e) { + /* ignore */ + } + + + this.mockMvc.perform(get("/actuator/prometheus")) + .andDo(print()) + .andExpect(status().isOk()); + /* test */ + for (String metric : List.of("dbr_access_give", "dbr_access_modify", "dbr_access_check", "dbr_access_delete")) { + assertThat(registry) + .hasObservationWithNameEqualTo(metric); + } + } + +} diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/MetricsConfig.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/MetricsConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..450be2f7df8b52fe493dd498dc0422350bb3ff39 --- /dev/null +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/MetricsConfig.java @@ -0,0 +1,15 @@ +package at.tuwien.config; + +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.aop.ObservedAspect; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MetricsConfig { + + @Bean + public ObservedAspect observedAspect(ObservationRegistry observationRegistry) { + return new ObservedAspect(observationRegistry); + } +}