diff --git a/fda-container-service/rest-service/src/main/java/at/tuwien/endpoints/ContainerEndpoint.java b/fda-container-service/rest-service/src/main/java/at/tuwien/endpoints/ContainerEndpoint.java index 12612c22e05806d2f4dd3d2dbf3e248b9268489f..1bbd9be6a3cf818565fe33ca590b7cc8f7c2f2b5 100644 --- a/fda-container-service/rest-service/src/main/java/at/tuwien/endpoints/ContainerEndpoint.java +++ b/fda-container-service/rest-service/src/main/java/at/tuwien/endpoints/ContainerEndpoint.java @@ -52,6 +52,7 @@ public class ContainerEndpoint { } @PostMapping + @PreAuthorize("hasRole('ROLE_RESEARCHER')") @ApiOperation(value = "Creates a new container", notes = "Creates a new container whose image is registered in the metadata database too.") @ApiResponses({ @ApiResponse(code = 201, message = "Successfully created a new container."), @@ -82,6 +83,7 @@ public class ContainerEndpoint { } @PutMapping("/{id}") + @PreAuthorize("hasRole('ROLE_RESEARCHER')") @ApiOperation(value = "Change the state of a container", notes = "The new state can only be one of START/STOP.") @ApiResponses({ @ApiResponse(code = 202, message = "Changed the state of a container."), diff --git a/fda-container-service/rest-service/src/main/java/at/tuwien/endpoints/ImageEndpoint.java b/fda-container-service/rest-service/src/main/java/at/tuwien/endpoints/ImageEndpoint.java index 09f7591ea9cb5e06fe4f189865c82b30a9e1ce48..701cb98379e53399b05234ef208ef07669a9f746 100644 --- a/fda-container-service/rest-service/src/main/java/at/tuwien/endpoints/ImageEndpoint.java +++ b/fda-container-service/rest-service/src/main/java/at/tuwien/endpoints/ImageEndpoint.java @@ -57,7 +57,7 @@ public class ImageEndpoint { } @PostMapping - @PreAuthorize("hasRole('DEVELOPER')") + @PreAuthorize("hasRole('ROLE_DEVELOPER')") @ApiOperation(value = "Creates a new image", notes = "Creates a new image in the metadata database.") @ApiResponses({ @ApiResponse(code = 201, message = "Successfully created a new image."), diff --git a/fda-database-service/rest-service/src/main/java/at/tuwien/endpoints/ContainerDatabaseEndpoint.java b/fda-database-service/rest-service/src/main/java/at/tuwien/endpoints/ContainerDatabaseEndpoint.java index cca4dca86db0a4864872943ded15141b355deea5..db25122bef1dfa2b6453715319d4cdba307aeac1 100644 --- a/fda-database-service/rest-service/src/main/java/at/tuwien/endpoints/ContainerDatabaseEndpoint.java +++ b/fda-database-service/rest-service/src/main/java/at/tuwien/endpoints/ContainerDatabaseEndpoint.java @@ -52,6 +52,7 @@ public class ContainerDatabaseEndpoint { } @PostMapping + @PreAuthorize("hasRole('ROLE_RESEARCHER')") @ApiOperation(value = "Creates a new database in a container", notes = "Creates a new database in a container. Note that the backend distincts between numerical (req: categories), nominal (req: max_length) and categorical (req: max_length, siUnit, min, max, mean, median, standard_deviation, histogram) column types.") @ApiResponses({ @ApiResponse(code = 201, message = "The database was successfully created."), diff --git a/fda-identifier-service/pom.xml b/fda-identifier-service/pom.xml index f69d3d5930dc2bc94561b4ca58772843e430c0c9..f59daef8532905c2b4771a98f7eef5a816d549f6 100644 --- a/fda-identifier-service/pom.xml +++ b/fda-identifier-service/pom.xml @@ -51,6 +51,16 @@ <artifactId>spring-cloud-starter-bootstrap</artifactId> <version>${spring-cloud.version}</version> </dependency> + + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-security</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.security</groupId> + <artifactId>spring-security-test</artifactId> + <scope>test</scope> + </dependency> <dependency> <groupId>javax.ws.rs</groupId> <artifactId>javax.ws.rs-api</artifactId> diff --git a/fda-identifier-service/rest-service/src/main/java/at/tuwien/endpoints/IdentifierEndpoint.java b/fda-identifier-service/rest-service/src/main/java/at/tuwien/endpoints/IdentifierEndpoint.java index 8f3d287871eff0e418cbb3a42292a4d61fc34450..18f795121b5dd921a7172e35fc9547f45ba63a63 100644 --- a/fda-identifier-service/rest-service/src/main/java/at/tuwien/endpoints/IdentifierEndpoint.java +++ b/fda-identifier-service/rest-service/src/main/java/at/tuwien/endpoints/IdentifierEndpoint.java @@ -12,6 +12,7 @@ import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; @@ -57,7 +58,8 @@ public class IdentifierEndpoint { } @PostMapping - @ApiOperation(value = "Create ID", notes = "Get Data from a Table in the database.") + @PreAuthorize("hasRole('ROLE_RESEARCHER') or hasRole('ROLE_DATA_STEWARD')") + @ApiOperation(value = "Create ID", notes = "Create a new identifier") @ApiResponses({ @ApiResponse(code = 201, message = "Created the ID."), @ApiResponse(code = 401, message = "Not authorized to update tables."), @@ -104,6 +106,7 @@ public class IdentifierEndpoint { } @DeleteMapping("/{identiferId}") + @PreAuthorize("hasRole('ROLE_DATA_STEWARD') or hasRole('ROLE_DEVELOPER')") @ApiOperation(value = "Delete ID", notes = "Get Data from a Table in the database.") @ApiResponses({ @ApiResponse(code = 200, message = "Get data from the table."), diff --git a/fda-identifier-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java b/fda-identifier-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..6bcb362bcd7ed5d338c250782c68145597fce5df --- /dev/null +++ b/fda-identifier-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java @@ -0,0 +1,56 @@ +package at.tuwien.auth; + +import at.tuwien.gateway.AuthenticationServiceGateway; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Slf4j +public class AuthTokenFilter extends OncePerRequestFilter { + + private final AuthenticationServiceGateway authenticationServiceGateway; + + public AuthTokenFilter(AuthenticationServiceGateway authenticationServiceGateway) { + this.authenticationServiceGateway = authenticationServiceGateway; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + String jwt = parseJwt(request); + log.debug("parsed jwt {}", jwt); + if (jwt != null) { + final UserDetails userDetails = authenticationServiceGateway.validate(jwt); + final UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + filterChain.doFilter(request, response); + } + + /** + * Parses the token from the HTTP header of the request + * + * @param request The request. + * @return The token. + */ + private String parseJwt(HttpServletRequest request) { + String headerAuth = request.getHeader("Authorization"); + if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) { + return headerAuth.substring(7, headerAuth.length()); + } + return null; + } +} \ No newline at end of file diff --git a/fda-identifier-service/services/src/main/java/at/tuwien/config/GatewayConfig.java b/fda-identifier-service/services/src/main/java/at/tuwien/config/GatewayConfig.java index daa14ed009686194a3f0e6edea0aed2f8424830c..252235a1eb97fcee790d7cf270935f06924f007f 100644 --- a/fda-identifier-service/services/src/main/java/at/tuwien/config/GatewayConfig.java +++ b/fda-identifier-service/services/src/main/java/at/tuwien/config/GatewayConfig.java @@ -1,23 +1,21 @@ package at.tuwien.config; -import lombok.Getter; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.DefaultUriBuilderFactory; -@Getter @Configuration public class GatewayConfig { - @Value("${fda.gateway.endpoint}") - private String gatewayEndpoint; + @Value("${fda.auth.endpoint}") + private String authEndpoint; @Bean public RestTemplate restTemplate() { final RestTemplate restTemplate = new RestTemplate(); - restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(gatewayEndpoint)); + restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(authEndpoint)); return restTemplate; } diff --git a/fda-identifier-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java b/fda-identifier-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..b5e82d5dc0ed126ab015896fe9cd8d9aa78a29e4 --- /dev/null +++ b/fda-identifier-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java @@ -0,0 +1,82 @@ +package at.tuwien.config; + +import at.tuwien.auth.AuthTokenFilter; +import at.tuwien.gateway.AuthenticationServiceGateway; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import javax.servlet.http.HttpServletResponse; + +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + + private final AuthenticationServiceGateway authenticationServiceGateway; + + @Autowired + public WebSecurityConfig(AuthenticationServiceGateway authenticationServiceGateway) { + this.authenticationServiceGateway = authenticationServiceGateway; + } + + @Bean + public AuthTokenFilter authTokenFilter() { + return new AuthTokenFilter(authenticationServiceGateway); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + /* enable CORS and disable CSRF */ + http = http.cors().and().csrf().disable(); + /* set session management to stateless */ + http = http + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and(); + /* set unauthorized requests exception handler */ + http = http + .exceptionHandling() + .authenticationEntryPoint( + (request, response, ex) -> { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, + ex.getMessage() + ); + } + ).and(); + /* set permissions on endpoints */ + http.authorizeRequests() + /* our public endpoints */ + .antMatchers(HttpMethod.GET, "/api/container/**/database/**/identifier/**").permitAll() + .antMatchers(HttpMethod.GET, "/api/pid/**").permitAll() + /* our private endpoints */ + .anyRequest().authenticated(); + /* add JWT token filter */ + http.addFilterBefore(authTokenFilter(), + UsernamePasswordAuthenticationFilter.class + ); + } + + @Bean + public CorsFilter corsFilter() { + final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + final CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + config.addAllowedOrigin("*"); + config.addAllowedHeader("*"); + config.addAllowedMethod("*"); + source.registerCorsConfiguration("/**", config); + return new CorsFilter(source); + } + +} diff --git a/fda-identifier-service/services/src/main/java/at/tuwien/gateway/AuthenticationServiceGateway.java b/fda-identifier-service/services/src/main/java/at/tuwien/gateway/AuthenticationServiceGateway.java new file mode 100644 index 0000000000000000000000000000000000000000..fc47ce246da851244cf1630b15f493ded20249d8 --- /dev/null +++ b/fda-identifier-service/services/src/main/java/at/tuwien/gateway/AuthenticationServiceGateway.java @@ -0,0 +1,14 @@ +package at.tuwien.gateway; + +import org.springframework.security.core.userdetails.UserDetails; + +public interface AuthenticationServiceGateway { + + /** + * Validates a token + * + * @param token The token + * @return User details on success + */ + UserDetails validate(String token); +} diff --git a/fda-identifier-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java b/fda-identifier-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..30507c4b6e662a7e772e85c609fdb5fbc563a99f --- /dev/null +++ b/fda-identifier-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java @@ -0,0 +1,36 @@ +package at.tuwien.gateway.impl; + +import at.tuwien.api.user.UserDto; +import at.tuwien.gateway.AuthenticationServiceGateway; +import at.tuwien.mapper.UserMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Service +public class AuthenticationServiceGatewayImpl implements AuthenticationServiceGateway { + + private final UserMapper userMapper; + private final RestTemplate restTemplate; + + @Autowired + public AuthenticationServiceGatewayImpl(UserMapper userMapper, RestTemplate restTemplate) { + this.userMapper = userMapper; + this.restTemplate = restTemplate; + } + + @Override + public UserDetails validate(String token) { + final HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "Bearer " + token); + final ResponseEntity<UserDto> response = restTemplate.exchange("/api/auth", HttpMethod.PUT, + new HttpEntity<>("", headers), UserDto.class); + return userMapper.userDtoToUserDetailsDto(response.getBody()); + } + +} diff --git a/fda-identifier-service/services/src/main/java/at/tuwien/mapper/UserMapper.java b/fda-identifier-service/services/src/main/java/at/tuwien/mapper/UserMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..f14430e9675c7a152c778df18fc892e3423afdc6 --- /dev/null +++ b/fda-identifier-service/services/src/main/java/at/tuwien/mapper/UserMapper.java @@ -0,0 +1,18 @@ +package at.tuwien.mapper; + +import at.tuwien.api.user.GrantedAuthorityDto; +import at.tuwien.api.user.UserDetailsDto; +import at.tuwien.api.user.UserDto; +import org.mapstruct.Mapper; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +@Mapper(componentModel = "spring") +public interface UserMapper { + + UserDetailsDto userDtoToUserDetailsDto(UserDto data); + + default GrantedAuthority grantedAuthorityDtoToGrantedAuthority(GrantedAuthorityDto data) { + return new SimpleGrantedAuthority(data.getAuthority()); + } +} diff --git a/fda-metadata-db/api/src/main/java/at/tuwien/api/database/query/ExecuteStatementDto.java b/fda-metadata-db/api/src/main/java/at/tuwien/api/database/query/ExecuteStatementDto.java index ea16db6f94a38d24cd58a6ac705f46ab1cda980d..41c49b17d642ac07c838be68e8fed6b4d78d1143 100644 --- a/fda-metadata-db/api/src/main/java/at/tuwien/api/database/query/ExecuteStatementDto.java +++ b/fda-metadata-db/api/src/main/java/at/tuwien/api/database/query/ExecuteStatementDto.java @@ -26,5 +26,5 @@ public class ExecuteStatementDto { @NotNull @ApiModelProperty(name = "columns mentioned in the query") - private List<List<ColumnBriefDto>> columns; + private List<ColumnBriefDto> columns; } diff --git a/fda-metadata-db/api/src/main/java/at/tuwien/api/database/query/QueryResultDto.java b/fda-metadata-db/api/src/main/java/at/tuwien/api/database/query/QueryResultDto.java index cfc176a8573e9b051ed21e0b68c2689e84d42e57..c2f4a798d75b2be0e60032ec27f4d8fb02e8ceff 100644 --- a/fda-metadata-db/api/src/main/java/at/tuwien/api/database/query/QueryResultDto.java +++ b/fda-metadata-db/api/src/main/java/at/tuwien/api/database/query/QueryResultDto.java @@ -4,7 +4,6 @@ import io.swagger.annotations.ApiModelProperty; import lombok.*; import javax.validation.constraints.NotNull; -import java.math.BigInteger; import java.util.List; import java.util.Map; @@ -17,9 +16,6 @@ import java.util.Map; @ToString public class QueryResultDto { - @ApiModelProperty(notes = "query count") - private BigInteger count; - @NotNull @ApiModelProperty(notes = "query result") private List<Map<String, Object>> result; diff --git a/fda-query-service/pom.xml b/fda-query-service/pom.xml index 1516cbcf8513df52a00fbf5636c462d611d09379..f1cd8e06ea4da4831e683351d3aee223680febb2 100644 --- a/fda-query-service/pom.xml +++ b/fda-query-service/pom.xml @@ -57,6 +57,15 @@ <artifactId>spring-cloud-starter-bootstrap</artifactId> <version>${spring-cloud.version}</version> </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-security</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.security</groupId> + <artifactId>spring-security-test</artifactId> + <scope>test</scope> + </dependency> <!-- Data Source --> <dependency> <groupId>org.postgresql</groupId> diff --git a/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/DataEndpoint.java b/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/DataEndpoint.java index a3fb4ec88c7ebfc63c864e5540f23fa643869e91..ca5838d52809bfca59f31ac9fff3508afb5aa5bc 100644 --- a/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/DataEndpoint.java +++ b/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/DataEndpoint.java @@ -3,7 +3,6 @@ package at.tuwien.endpoint; import at.tuwien.api.database.query.QueryResultDto; import at.tuwien.api.database.table.TableCsvDto; import at.tuwien.exception.*; -import at.tuwien.service.CommaValueService; import at.tuwien.service.QueryService; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiResponse; @@ -16,28 +15,25 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import javax.transaction.Transactional; import javax.validation.Valid; import javax.validation.constraints.NotNull; +import java.math.BigInteger; import java.time.Instant; @Log4j2 @CrossOrigin(origins = "*") @RestController -@RequestMapping("/api/container/{id}/database/{databaseId}/table/{tableId}/data") +@RequestMapping("/api/container/{id}/database/{databaseId}/data") public class DataEndpoint { private final QueryService queryService; - private final CommaValueService commaValueService; @Autowired - public DataEndpoint(QueryService queryService, CommaValueService commaValueService) { + public DataEndpoint(QueryService queryService) { this.queryService = queryService; - this.commaValueService = commaValueService; } - @Transactional - @PostMapping + @PostMapping("/api/container/{id}/database/{databaseId}/table/{tableId}/data") @ApiOperation(value = "Insert values", notes = "Insert Data into a Table in the database. When the location string is set, the data argument is ignored and the location is used as data input") @ApiResponses({ @ApiResponse(code = 201, message = "Updated the table."), @@ -67,8 +63,7 @@ public class DataEndpoint { .body(queryService.insert(id, databaseId, tableId, data)); } - @Transactional - @GetMapping + @GetMapping("/api/container/{id}/database/{databaseId}/table/{tableId}/data") @ApiOperation(value = "Get values", notes = "Get Data from a Table in the database.") @ApiResponses({ @ApiResponse(code = 200, message = "Get data from the table."), @@ -80,16 +75,10 @@ public class DataEndpoint { @NotNull @PathVariable("databaseId") Long databaseId, @NotNull @PathVariable("tableId") Long tableId, @RequestParam(required = false) Instant timestamp, - @RequestParam(required = false) String total, @RequestParam(required = false) Long page, @RequestParam(required = false) Long size) throws TableNotFoundException, DatabaseNotFoundException, DatabaseConnectionException, ImageNotSupportedException, TableMalformedException, PaginationException, ContainerNotFoundException { - if (total != null) { - /* we ignore the value */ - final QueryResultDto response = queryService.count(id, databaseId, tableId, timestamp); - return ResponseEntity.ok(response); - } if ((page == null && size != null) || (page != null && size == null)) { log.error("Cannot perform pagination with only one of page/size set."); log.debug("invalid pagination specification, one of page/size is null, either both should be null or none."); @@ -101,12 +90,41 @@ public class DataEndpoint { if (size != null && size <= 0) { throw new PaginationException("Page number cannot be lower or equal to 0"); } + final BigInteger count = queryService.count(id, databaseId, tableId, timestamp); + final HttpHeaders headers = new HttpHeaders(); + headers.set("FDA-COUNT", count.toString()); final QueryResultDto response = queryService.findAll(id, databaseId, tableId, timestamp, page, size); - return ResponseEntity.ok(response); + return ResponseEntity.ok() + .headers(headers) + .body(response); + } + + @RequestMapping(value = "/api/container/{id}/database/{databaseId}/table/{tableId}/data", method = RequestMethod.HEAD) + @ApiOperation(value = "Get values", notes = "Get Data Count from a Table in the database.") + @ApiResponses({ + @ApiResponse(code = 200, message = "Get data from the table."), + @ApiResponse(code = 401, message = "Not authorized to update tables."), + @ApiResponse(code = 404, message = "The table is not found in database."), + @ApiResponse(code = 405, message = "The connection to the database was unsuccessful."), + }) + public ResponseEntity<QueryResultDto> getCount(@NotNull @PathVariable("id") Long id, + @NotNull @PathVariable("databaseId") Long databaseId, + @NotNull @PathVariable("tableId") Long tableId, + @RequestParam(required = false) Instant timestamp) + throws TableNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, + TableMalformedException, ContainerNotFoundException { + final BigInteger count = queryService.count(id, databaseId, tableId, timestamp); + final HttpHeaders headers = new HttpHeaders(); + headers.set("FDA-COUNT", count.toString()); + return ResponseEntity.ok() + .headers(headers) + .build(); } - @Transactional - @GetMapping(value = "/export") + /** + * todo use dbs internal export functionality + */ + @GetMapping(value = "/api/container/{id}/database/{databaseId}/table/{tableId}/export") @ApiOperation(value = "Download export", notes = "Get Data from a Table in the database.") @ApiResponses({ @ApiResponse(code = 200, message = "Get data from the table."), @@ -121,10 +139,11 @@ public class DataEndpoint { throws TableNotFoundException, DatabaseNotFoundException, DatabaseConnectionException, ImageNotSupportedException, TableMalformedException, FileStorageException, PaginationException, ContainerNotFoundException { - final InputStreamResource data = commaValueService.export(id, databaseId, tableId, timestamp); - HttpHeaders headers = new HttpHeaders(); - headers.add("Content-Disposition", "attachment; filename=\"export.csv\""); - return new ResponseEntity<>(data, headers, HttpStatus.OK); +// final HttpHeaders headers = new HttpHeaders(); +// headers.add("Content-Disposition", "attachment; filename=\"export.csv\""); +// return new ResponseEntity<>(data, headers, HttpStatus.OK); + return ResponseEntity.status(HttpStatus.NOT_IMPLEMENTED) + .build(); } diff --git a/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/QueryEndpoint.java b/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/QueryEndpoint.java index 7a1d20dda101098809c3e6cf4d7f53716991e28f..2bdaddedb36c523f765ddc6c369d73a1aa4064cb 100644 --- a/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/QueryEndpoint.java +++ b/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/QueryEndpoint.java @@ -20,7 +20,7 @@ import javax.validation.constraints.NotNull; @Log4j2 @RestController -@RequestMapping("/api/container/{id}/database/{databaseId}/table/{tableId}/query") +@RequestMapping("/api/container/{id}/database/{databaseId}/query") public class QueryEndpoint { private final QueryMapper queryMapper; @@ -43,7 +43,6 @@ public class QueryEndpoint { @ApiResponse(code = 409, message = "The container image is not supported."),}) public ResponseEntity<QueryResultDto> execute(@NotNull @PathVariable("id") Long id, @NotNull @PathVariable("databaseId") Long databaseId, - @NotNull @PathVariable("tableId") Long tableId, @NotNull @RequestBody @Valid ExecuteStatementDto data) throws DatabaseNotFoundException, ImageNotSupportedException, QueryStoreException, QueryMalformedException, TableNotFoundException, ContainerNotFoundException { @@ -56,7 +55,7 @@ public class QueryEndpoint { log.error("Table list is empty"); throw new QueryMalformedException("Invalid table"); } - final QueryResultDto result = queryService.execute(id, databaseId, tableId, data); + final QueryResultDto result = queryService.execute(id, databaseId, data); final QueryDto query = queryMapper.queryToQueryDto(storeService.insert(id, databaseId, result, data)); result.setId(query.getId()); return ResponseEntity.status(HttpStatus.ACCEPTED) @@ -72,7 +71,6 @@ public class QueryEndpoint { @ApiResponse(code = 409, message = "The container image is not supported."),}) public ResponseEntity<QueryDto> save(@NotNull @PathVariable("id") Long id, @NotNull @PathVariable("databaseId") Long databaseId, - @NotNull @PathVariable("tableId") Long tableId, @NotNull @RequestBody SaveStatementDto data) throws DatabaseNotFoundException, ImageNotSupportedException, QueryStoreException, ContainerNotFoundException { @@ -91,14 +89,13 @@ public class QueryEndpoint { @ApiResponse(code = 409, message = "The container image is not supported."),}) public ResponseEntity<QueryResultDto> reExecute(@NotNull @PathVariable("id") Long id, @NotNull @PathVariable("databaseId") Long databaseId, - @NotNull @PathVariable("tableId") Long tableId, @NotNull @PathVariable("queryId") Long queryId) throws QueryStoreException, QueryNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, TableNotFoundException, QueryMalformedException, ContainerNotFoundException { final Query query = storeService.findOne(id, databaseId, queryId); final QueryDto queryDto = queryMapper.queryToQueryDto(query); final ExecuteStatementDto statement = queryMapper.queryDtoToExecuteStatementDto(queryDto); - final QueryResultDto result = queryService.execute(id, databaseId, tableId, statement); + final QueryResultDto result = queryService.execute(id, databaseId, statement); result.setId(queryId); return ResponseEntity.status(HttpStatus.ACCEPTED) .body(result); diff --git a/fda-query-service/rest-service/src/test/java/at/tuwien/endpoint/DataEndpointUnitTest.java b/fda-query-service/rest-service/src/test/java/at/tuwien/endpoint/DataEndpointUnitTest.java index 961e19d879d98a93072928cc50f967efe8872b71..545c8428d7c6235d850bb0393517ad3a1232a691 100644 --- a/fda-query-service/rest-service/src/test/java/at/tuwien/endpoint/DataEndpointUnitTest.java +++ b/fda-query-service/rest-service/src/test/java/at/tuwien/endpoint/DataEndpointUnitTest.java @@ -97,7 +97,7 @@ public class DataEndpointUnitTest extends BaseUnitTest { DatabaseNotFoundException, ImageNotSupportedException, PaginationException, ContainerNotFoundException { /* test */ - dataEndpoint.getAll(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, null, null, null, null); + dataEndpoint.getAll(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, null, null, null); } @Test @@ -108,7 +108,7 @@ public class DataEndpointUnitTest extends BaseUnitTest { final Long size = null; /* test */ - dataEndpoint.getAll(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, DATABASE_1_CREATED, null, page, size); + dataEndpoint.getAll(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, DATABASE_1_CREATED, page, size); } @Test @@ -118,7 +118,7 @@ public class DataEndpointUnitTest extends BaseUnitTest { /* test */ assertThrows(PaginationException.class, () -> { - dataEndpoint.getAll(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, DATABASE_1_CREATED, null, page, size); + dataEndpoint.getAll(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, DATABASE_1_CREATED, page, size); }); } @@ -129,7 +129,7 @@ public class DataEndpointUnitTest extends BaseUnitTest { /* test */ assertThrows(PaginationException.class, () -> { - dataEndpoint.getAll(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, DATABASE_1_CREATED, null, page, size); + dataEndpoint.getAll(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, DATABASE_1_CREATED, page, size); }); } @@ -140,7 +140,7 @@ public class DataEndpointUnitTest extends BaseUnitTest { /* test */ assertThrows(PaginationException.class, () -> { - dataEndpoint.getAll(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, DATABASE_1_CREATED, null, page, size); + dataEndpoint.getAll(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, DATABASE_1_CREATED, page, size); }); } @@ -151,7 +151,7 @@ public class DataEndpointUnitTest extends BaseUnitTest { /* test */ assertThrows(PaginationException.class, () -> { - dataEndpoint.getAll(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, DATABASE_1_CREATED, null, page, size); + dataEndpoint.getAll(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, DATABASE_1_CREATED, page, size); }); } @@ -162,7 +162,7 @@ public class DataEndpointUnitTest extends BaseUnitTest { /* test */ assertThrows(PaginationException.class, () -> { - dataEndpoint.getAll(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, DATABASE_1_CREATED, null, page, size); + dataEndpoint.getAll(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, DATABASE_1_CREATED, page, size); }); } @@ -173,7 +173,7 @@ public class DataEndpointUnitTest extends BaseUnitTest { /* test */ assertThrows(PaginationException.class, () -> { - dataEndpoint.getAll(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, DATABASE_1_CREATED, null, page, size); + dataEndpoint.getAll(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, DATABASE_1_CREATED, page, size); }); } @@ -184,7 +184,7 @@ public class DataEndpointUnitTest extends BaseUnitTest { /* test */ assertThrows(PaginationException.class, () -> { - dataEndpoint.getAll(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, DATABASE_1_CREATED, null, page, size); + dataEndpoint.getAll(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, DATABASE_1_CREATED, page, size); }); } @@ -194,10 +194,10 @@ public class DataEndpointUnitTest extends BaseUnitTest { PaginationException, ContainerNotFoundException { /* test */ - final ResponseEntity<InputStreamResource> respone = dataEndpoint.export(CONTAINER_1_ID, DATABASE_1_ID, + final ResponseEntity<InputStreamResource> response = dataEndpoint.export(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, null); - assertNotNull(respone); - assertEquals(HttpStatus.OK, respone.getStatusCode()); + assertNotNull(response); + assertEquals(HttpStatus.OK, response.getStatusCode()); } @Test @@ -208,10 +208,10 @@ public class DataEndpointUnitTest extends BaseUnitTest { .minusMillis(1000 * 1000); /* test */ - final ResponseEntity<InputStreamResource> respone = dataEndpoint.export(CONTAINER_1_ID, DATABASE_1_ID, + final ResponseEntity<InputStreamResource> response = dataEndpoint.export(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, request); - assertNotNull(respone); - assertEquals(HttpStatus.OK, respone.getStatusCode()); + assertNotNull(response); + assertEquals(HttpStatus.OK, response.getStatusCode()); } @@ -223,24 +223,37 @@ public class DataEndpointUnitTest extends BaseUnitTest { .plusMillis(1000 * 1000); /* test */ - final ResponseEntity<InputStreamResource> respone = dataEndpoint.export(CONTAINER_1_ID, DATABASE_1_ID, + final ResponseEntity<InputStreamResource> response = dataEndpoint.export(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, request); - assertNotNull(respone); - assertEquals(HttpStatus.OK, respone.getStatusCode()); + assertNotNull(response); + assertEquals(HttpStatus.OK, response.getStatusCode()); } @Test public void getAllTotal_succeeds() throws TableNotFoundException, DatabaseConnectionException, - TableMalformedException, DatabaseNotFoundException, ImageNotSupportedException, FileStorageException, + TableMalformedException, DatabaseNotFoundException, ImageNotSupportedException, PaginationException, ContainerNotFoundException { final Instant timestamp = Instant.now(); - final String total = "1"; /* test */ - final ResponseEntity<QueryResultDto> respone = dataEndpoint.getAll(CONTAINER_1_ID, DATABASE_1_ID, - TABLE_1_ID, timestamp, total, null, null); - assertNotNull(respone); - assertEquals(HttpStatus.OK, respone.getStatusCode()); + final ResponseEntity<QueryResultDto> response = dataEndpoint.getAll(CONTAINER_1_ID, DATABASE_1_ID, + TABLE_1_ID, timestamp, null, null); + assertNotNull(response); + assertEquals(HttpStatus.OK, response.getStatusCode()); + } + + @Test + public void getAllCount_succeeds() throws TableNotFoundException, DatabaseConnectionException, + TableMalformedException, DatabaseNotFoundException, ImageNotSupportedException, + PaginationException, ContainerNotFoundException { + final Instant timestamp = Instant.now(); + + /* test */ + final ResponseEntity<QueryResultDto> response = dataEndpoint.getAll(CONTAINER_1_ID, DATABASE_1_ID, + TABLE_1_ID, timestamp, null, null); + assertNotNull(response); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertTrue(response.getHeaders().containsKey("FDA-COUNT")); } } diff --git a/fda-query-service/rest-service/src/test/java/at/tuwien/endpoint/QueryEndpointIntegrationTest.java b/fda-query-service/rest-service/src/test/java/at/tuwien/endpoint/QueryEndpointIntegrationTest.java index 8e435df1de50823b2080542a7325e7224190d8bd..93bb02dd2c6bcdedbe83d1fc20ca01493dcce74e 100644 --- a/fda-query-service/rest-service/src/test/java/at/tuwien/endpoint/QueryEndpointIntegrationTest.java +++ b/fda-query-service/rest-service/src/test/java/at/tuwien/endpoint/QueryEndpointIntegrationTest.java @@ -150,7 +150,7 @@ public class QueryEndpointIntegrationTest extends BaseUnitTest { /* test */ final ResponseEntity<QueryResultDto> response = queryEndpoint.reExecute(CONTAINER_1_ID, DATABASE_1_ID, - TABLE_1_ID, QUERY_1_ID); + QUERY_1_ID); assertEquals(HttpStatus.ACCEPTED, response.getStatusCode()); assertNotNull(response.getBody()); assertEquals(QUERY_1_ID, response.getBody().getId()); @@ -168,8 +168,7 @@ public class QueryEndpointIntegrationTest extends BaseUnitTest { MariaDbConfig.clearQueryStore(TABLE_1); /* test */ - final ResponseEntity<QueryDto> response = queryEndpoint.save(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, - statement); + final ResponseEntity<QueryDto> response = queryEndpoint.save(CONTAINER_1_ID, DATABASE_1_ID, statement); assertEquals(HttpStatus.ACCEPTED, response.getStatusCode()); assertNotNull(response.getBody()); assertEquals(QUERY_1_ID, response.getBody().getId()); diff --git a/fda-query-service/rest-service/src/test/java/at/tuwien/endpoint/QueryEndpointUnitTest.java b/fda-query-service/rest-service/src/test/java/at/tuwien/endpoint/QueryEndpointUnitTest.java index 5376d7d145a3f88e758cd9dfd3e422ea5a9043fa..061853cd7732d64a04e97f05ddad03da4d6fbc44 100644 --- a/fda-query-service/rest-service/src/test/java/at/tuwien/endpoint/QueryEndpointUnitTest.java +++ b/fda-query-service/rest-service/src/test/java/at/tuwien/endpoint/QueryEndpointUnitTest.java @@ -58,13 +58,13 @@ public class QueryEndpointUnitTest extends BaseUnitTest { .build(); /* mock */ - when(queryService.execute(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, request)) + when(queryService.execute(CONTAINER_1_ID, DATABASE_1_ID, request)) .thenReturn(result); when(storeService.insert(CONTAINER_1_ID, DATABASE_1_ID, result, request)) .thenReturn(QUERY_1); /* test */ - final ResponseEntity<QueryResultDto> response = queryEndpoint.execute(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, request); + final ResponseEntity<QueryResultDto> response = queryEndpoint.execute(CONTAINER_1_ID, DATABASE_1_ID, request); assertEquals(HttpStatus.ACCEPTED, response.getStatusCode()); assertEquals(result, response.getBody()); } @@ -81,13 +81,13 @@ public class QueryEndpointUnitTest extends BaseUnitTest { .build(); /* mock */ - when(queryService.execute(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, request)) + when(queryService.execute(CONTAINER_1_ID, DATABASE_1_ID, request)) .thenReturn(result); when(storeService.insert(CONTAINER_1_ID, DATABASE_1_ID, result, request)) .thenReturn(QUERY_1); /* test */ - final ResponseEntity<QueryResultDto> response = queryEndpoint.execute(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, request); + final ResponseEntity<QueryResultDto> response = queryEndpoint.execute(CONTAINER_1_ID, DATABASE_1_ID, request); assertEquals(HttpStatus.ACCEPTED, response.getStatusCode()); assertEquals(result, response.getBody()); } @@ -100,12 +100,12 @@ public class QueryEndpointUnitTest extends BaseUnitTest { .build(); /* mock */ - when(queryService.execute(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, request)) + when(queryService.execute(CONTAINER_1_ID, DATABASE_1_ID, request)) .thenThrow(TableNotFoundException.class); /* test */ assertThrows(TableNotFoundException.class, () -> { - queryEndpoint.execute(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, request); + queryEndpoint.execute(CONTAINER_1_ID, DATABASE_1_ID, request); }); } @@ -121,7 +121,7 @@ public class QueryEndpointUnitTest extends BaseUnitTest { .thenReturn(QUERY_1); /* test */ - final ResponseEntity<QueryDto> response = queryEndpoint.save(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, request); + final ResponseEntity<QueryDto> response = queryEndpoint.save(CONTAINER_1_ID, DATABASE_1_ID, request); assertEquals(HttpStatus.ACCEPTED, response.getStatusCode()); assertEquals(QUERY_1_DTO, response.getBody()); } @@ -139,7 +139,7 @@ public class QueryEndpointUnitTest extends BaseUnitTest { /* test */ assertThrows(DatabaseNotFoundException.class, () -> { - queryEndpoint.save(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, request); + queryEndpoint.save(CONTAINER_1_ID, DATABASE_1_ID, request); }); } diff --git a/fda-query-service/rest-service/src/test/java/at/tuwien/service/DataServiceIntegrationTest.java b/fda-query-service/rest-service/src/test/java/at/tuwien/service/DataServiceIntegrationTest.java index 14a4204d66e8d2f58a185c6804549874d298eb4c..69ed0e5dcc78fde81e5a9653c8462131d573b7f1 100644 --- a/fda-query-service/rest-service/src/test/java/at/tuwien/service/DataServiceIntegrationTest.java +++ b/fda-query-service/rest-service/src/test/java/at/tuwien/service/DataServiceIntegrationTest.java @@ -13,15 +13,11 @@ import com.github.dockerjava.api.model.Bind; import com.github.dockerjava.api.model.Network; import com.rabbitmq.client.Channel; import lombok.extern.log4j.Log4j2; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.*; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.core.io.Resource; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -31,7 +27,6 @@ import java.util.Arrays; import static at.tuwien.config.DockerConfig.dockerClient; import static at.tuwien.config.DockerConfig.hostConfig; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; @Log4j2 @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) @@ -134,13 +129,14 @@ public class DataServiceIntegrationTest extends BaseUnitTest { } @Test + @Disabled public void write_succeeds() throws TableNotFoundException, DatabaseConnectionException, TableMalformedException, DatabaseNotFoundException, ImageNotSupportedException, FileStorageException, PaginationException, ContainerNotFoundException { /* test */ - final Resource response = dataService.export(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID); - assertTrue(response.exists()); +// final Resource response = dataService.export(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID); +// assertTrue(response.exists()); } @Test diff --git a/fda-query-service/rest-service/src/test/java/at/tuwien/service/QueryServiceIntegrationTest.java b/fda-query-service/rest-service/src/test/java/at/tuwien/service/QueryServiceIntegrationTest.java index c27f890cf32a0f3dc1cd5e324048b3da0e79a9d4..50d55717bd76dbba50399ea0c6899f2c496cb1dc 100644 --- a/fda-query-service/rest-service/src/test/java/at/tuwien/service/QueryServiceIntegrationTest.java +++ b/fda-query-service/rest-service/src/test/java/at/tuwien/service/QueryServiceIntegrationTest.java @@ -187,7 +187,7 @@ public class QueryServiceIntegrationTest extends BaseUnitTest { DockerConfig.startContainer(CONTAINER_1); /* test */ - final QueryResultDto response = queryService.execute(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, request); + final QueryResultDto response = queryService.execute(CONTAINER_1_ID, DATABASE_1_ID, request); assertEquals(3, response.getResult().size()); assertEquals(BigInteger.valueOf(1L), response.getResult().get(0).get(COLUMN_1_1_NAME)); assertEquals(toInstant("2008-12-01"), response.getResult().get(0).get(COLUMN_1_2_NAME)); @@ -220,26 +220,11 @@ public class QueryServiceIntegrationTest extends BaseUnitTest { DockerConfig.startContainer(CONTAINER_1); /* test */ - final QueryResultDto response = queryService.execute(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, request); + final QueryResultDto response = queryService.execute(CONTAINER_1_ID, DATABASE_1_ID, request); assertNotNull(response.getResult()); assertEquals(3, response.getResult().size()); } - @Test - public void execute_tableNotExists_fails() throws InterruptedException { - final ExecuteStatementDto request = ExecuteStatementDto.builder() - .statement(QUERY_1_STATEMENT) - .build(); - - /* mock */ - DockerConfig.startContainer(CONTAINER_1); - - /* test */ - assertThrows(TableNotFoundException.class, () -> { - queryService.execute(CONTAINER_1_ID, DATABASE_1_ID, 9999L, request); - }); - } - @Test public void execute_databaseNotExists_fails() throws InterruptedException { final ExecuteStatementDto request = ExecuteStatementDto.builder() @@ -251,7 +236,7 @@ public class QueryServiceIntegrationTest extends BaseUnitTest { /* test */ assertThrows(DatabaseNotFoundException.class, () -> { - queryService.execute(CONTAINER_1_ID, 9999L, TABLE_1_ID, request); + queryService.execute(CONTAINER_1_ID, 9999L, request); }); } @@ -267,7 +252,7 @@ public class QueryServiceIntegrationTest extends BaseUnitTest { /* test */ assertThrows(PersistenceException.class, () -> { - queryService.execute(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, request); + queryService.execute(CONTAINER_1_ID, DATABASE_1_ID, request); }); } @@ -282,7 +267,7 @@ public class QueryServiceIntegrationTest extends BaseUnitTest { /* test */ assertThrows(PersistenceException.class, () -> { - queryService.execute(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, request); + queryService.execute(CONTAINER_1_ID, DATABASE_1_ID, request); }); } @@ -297,7 +282,7 @@ public class QueryServiceIntegrationTest extends BaseUnitTest { /* test */ assertThrows(QueryMalformedException.class, () -> { - queryService.execute(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, request); + queryService.execute(CONTAINER_1_ID, DATABASE_1_ID, request); }); } diff --git a/fda-query-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java b/fda-query-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..6bcb362bcd7ed5d338c250782c68145597fce5df --- /dev/null +++ b/fda-query-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java @@ -0,0 +1,56 @@ +package at.tuwien.auth; + +import at.tuwien.gateway.AuthenticationServiceGateway; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Slf4j +public class AuthTokenFilter extends OncePerRequestFilter { + + private final AuthenticationServiceGateway authenticationServiceGateway; + + public AuthTokenFilter(AuthenticationServiceGateway authenticationServiceGateway) { + this.authenticationServiceGateway = authenticationServiceGateway; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + String jwt = parseJwt(request); + log.debug("parsed jwt {}", jwt); + if (jwt != null) { + final UserDetails userDetails = authenticationServiceGateway.validate(jwt); + final UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + filterChain.doFilter(request, response); + } + + /** + * Parses the token from the HTTP header of the request + * + * @param request The request. + * @return The token. + */ + private String parseJwt(HttpServletRequest request) { + String headerAuth = request.getHeader("Authorization"); + if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) { + return headerAuth.substring(7, headerAuth.length()); + } + return null; + } +} \ No newline at end of file diff --git a/fda-query-service/services/src/main/java/at/tuwien/config/GatewayConfig.java b/fda-query-service/services/src/main/java/at/tuwien/config/GatewayConfig.java index daa14ed009686194a3f0e6edea0aed2f8424830c..252235a1eb97fcee790d7cf270935f06924f007f 100644 --- a/fda-query-service/services/src/main/java/at/tuwien/config/GatewayConfig.java +++ b/fda-query-service/services/src/main/java/at/tuwien/config/GatewayConfig.java @@ -1,23 +1,21 @@ package at.tuwien.config; -import lombok.Getter; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.DefaultUriBuilderFactory; -@Getter @Configuration public class GatewayConfig { - @Value("${fda.gateway.endpoint}") - private String gatewayEndpoint; + @Value("${fda.auth.endpoint}") + private String authEndpoint; @Bean public RestTemplate restTemplate() { final RestTemplate restTemplate = new RestTemplate(); - restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(gatewayEndpoint)); + restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(authEndpoint)); return restTemplate; } diff --git a/fda-query-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java b/fda-query-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..b26f8e2994bb2378aaa914d38daad30b567476a6 --- /dev/null +++ b/fda-query-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java @@ -0,0 +1,84 @@ +package at.tuwien.config; + +import at.tuwien.auth.AuthTokenFilter; +import at.tuwien.gateway.AuthenticationServiceGateway; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import javax.servlet.http.HttpServletResponse; + +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + + private final AuthenticationServiceGateway authenticationServiceGateway; + + @Autowired + public WebSecurityConfig(AuthenticationServiceGateway authenticationServiceGateway) { + this.authenticationServiceGateway = authenticationServiceGateway; + } + + @Bean + public AuthTokenFilter authTokenFilter() { + return new AuthTokenFilter(authenticationServiceGateway); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + /* enable CORS and disable CSRF */ + http = http.cors().and().csrf().disable(); + /* set session management to stateless */ + http = http + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and(); + /* set unauthorized requests exception handler */ + http = http + .exceptionHandling() + .authenticationEntryPoint( + (request, response, ex) -> { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, + ex.getMessage() + ); + } + ).and(); + /* set permissions on endpoints */ + http.authorizeRequests() + /* our public endpoints */ + .antMatchers(HttpMethod.GET, "/api/container/**/database/data/**").permitAll() + .antMatchers(HttpMethod.GET, "/api/container/**/database/query/**").permitAll() + .antMatchers(HttpMethod.GET, "/api/container/**/database/**/query/**").permitAll() + .antMatchers(HttpMethod.GET, "/api/container/**/database/**/version/**").permitAll() + /* our private endpoints */ + .anyRequest().authenticated(); + /* add JWT token filter */ + http.addFilterBefore(authTokenFilter(), + UsernamePasswordAuthenticationFilter.class + ); + } + + @Bean + public CorsFilter corsFilter() { + final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + final CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + config.addAllowedOrigin("*"); + config.addAllowedHeader("*"); + config.addAllowedMethod("*"); + source.registerCorsConfiguration("/**", config); + return new CorsFilter(source); + } + +} diff --git a/fda-query-service/services/src/main/java/at/tuwien/gateway/AuthenticationServiceGateway.java b/fda-query-service/services/src/main/java/at/tuwien/gateway/AuthenticationServiceGateway.java new file mode 100644 index 0000000000000000000000000000000000000000..fc47ce246da851244cf1630b15f493ded20249d8 --- /dev/null +++ b/fda-query-service/services/src/main/java/at/tuwien/gateway/AuthenticationServiceGateway.java @@ -0,0 +1,14 @@ +package at.tuwien.gateway; + +import org.springframework.security.core.userdetails.UserDetails; + +public interface AuthenticationServiceGateway { + + /** + * Validates a token + * + * @param token The token + * @return User details on success + */ + UserDetails validate(String token); +} diff --git a/fda-query-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java b/fda-query-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..30507c4b6e662a7e772e85c609fdb5fbc563a99f --- /dev/null +++ b/fda-query-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java @@ -0,0 +1,36 @@ +package at.tuwien.gateway.impl; + +import at.tuwien.api.user.UserDto; +import at.tuwien.gateway.AuthenticationServiceGateway; +import at.tuwien.mapper.UserMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Service +public class AuthenticationServiceGatewayImpl implements AuthenticationServiceGateway { + + private final UserMapper userMapper; + private final RestTemplate restTemplate; + + @Autowired + public AuthenticationServiceGatewayImpl(UserMapper userMapper, RestTemplate restTemplate) { + this.userMapper = userMapper; + this.restTemplate = restTemplate; + } + + @Override + public UserDetails validate(String token) { + final HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "Bearer " + token); + final ResponseEntity<UserDto> response = restTemplate.exchange("/api/auth", HttpMethod.PUT, + new HttpEntity<>("", headers), UserDto.class); + return userMapper.userDtoToUserDetailsDto(response.getBody()); + } + +} diff --git a/fda-query-service/services/src/main/java/at/tuwien/mapper/QueryMapper.java b/fda-query-service/services/src/main/java/at/tuwien/mapper/QueryMapper.java index 7b512825f2aaa4e5030bbd2af3ce81bb0c0d45ce..112effd8ba11e1ba7619b354d86c1fda86d60705 100644 --- a/fda-query-service/services/src/main/java/at/tuwien/mapper/QueryMapper.java +++ b/fda-query-service/services/src/main/java/at/tuwien/mapper/QueryMapper.java @@ -53,7 +53,7 @@ public interface QueryMapper { return slug.toLowerCase(Locale.ENGLISH); } - default QueryResultDto resultListToQueryResultDto(Table table, List<?> result, ExecuteStatementDto metadata) { + default QueryResultDto resultListToQueryResultDto(List<TableColumn> columns, List<?> result) { final Iterator<?> iterator = result.iterator(); final List<Map<String, Object>> resultList = new LinkedList<>(); while (iterator.hasNext()) { @@ -61,17 +61,11 @@ public interface QueryMapper { int[] idx = new int[]{0}; final Object[] data = (Object[]) iterator.next(); final Map<String, Object> map = new HashMap<>(); - final List<TableColumn> cols = table.getColumns() /* todo extend for more than 1 table */; - metadata.getColumns() - .forEach(columns -> columns.forEach(column -> map.put(column.getName(), - dataColumnToObject(data[idx[0]++], cols.stream().filter(c -> c.getId() - .equals(column.getId())) - .findFirst() - .get())))); + columns + .forEach(column -> map.put(column.getName(), + dataColumnToObject(data[idx[0]++], column))); resultList.add(map); } - log.info("Display data for table id {}", table.getId()); - log.trace("table {} contains {} records", table, resultList.size()); return QueryResultDto.builder() .result(resultList) .build(); @@ -223,7 +217,6 @@ public interface QueryMapper { log.trace("table {} contains {} records", table, queryResult.size()); return QueryResultDto.builder() .result(queryResult) - .count(BigInteger.valueOf(queryResult.size())) .build(); } diff --git a/fda-query-service/services/src/main/java/at/tuwien/mapper/UserMapper.java b/fda-query-service/services/src/main/java/at/tuwien/mapper/UserMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..f14430e9675c7a152c778df18fc892e3423afdc6 --- /dev/null +++ b/fda-query-service/services/src/main/java/at/tuwien/mapper/UserMapper.java @@ -0,0 +1,18 @@ +package at.tuwien.mapper; + +import at.tuwien.api.user.GrantedAuthorityDto; +import at.tuwien.api.user.UserDetailsDto; +import at.tuwien.api.user.UserDto; +import org.mapstruct.Mapper; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +@Mapper(componentModel = "spring") +public interface UserMapper { + + UserDetailsDto userDtoToUserDetailsDto(UserDto data); + + default GrantedAuthority grantedAuthorityDtoToGrantedAuthority(GrantedAuthorityDto data) { + return new SimpleGrantedAuthority(data.getAuthority()); + } +} diff --git a/fda-query-service/services/src/main/java/at/tuwien/service/CommaValueService.java b/fda-query-service/services/src/main/java/at/tuwien/service/CommaValueService.java index 94a766e5fa3f1db933e396c9e858bfae63c15806..c06664088e68b162926d3d24baff765efb39e894 100644 --- a/fda-query-service/services/src/main/java/at/tuwien/service/CommaValueService.java +++ b/fda-query-service/services/src/main/java/at/tuwien/service/CommaValueService.java @@ -2,12 +2,7 @@ package at.tuwien.service; import at.tuwien.api.database.table.TableCsvDto; import at.tuwien.exception.*; -import com.opencsv.exceptions.CsvException; import org.springframework.core.io.InputStreamResource; -import org.springframework.core.io.Resource; - -import java.io.IOException; -import java.time.Instant; public interface CommaValueService { @@ -46,41 +41,41 @@ public interface CommaValueService { TableCsvDto read(Long containerId, Long databaseId, Long tableId, String location, Character separator, Long skipLines, String nullElement, String falseElement, String trueElement) throws TableNotFoundException, DatabaseNotFoundException, FileStorageException, ContainerNotFoundException; - /** - * Exports a table to a file by given database and table id for a specific point in time. - * - * @param containerId The container id. - * @param databaseId The database id. - * @param tableId The table id. - * @param timestamp The point in time. - * @return The export. - * @throws TableNotFoundException The table was not found. - * @throws DatabaseConnectionException The connection to the database failed. - * @throws DatabaseNotFoundException The database was not found. - * @throws ImageNotSupportedException The image is not supported. Currently only MariaDB is supported. - * @throws PaginationException The pagination failed. - * @throws FileStorageException The table could not be exported. - * @throws TableMalformedException The table is malformed. - */ - InputStreamResource export(Long containerId, Long databaseId, Long tableId, Instant timestamp) throws TableNotFoundException, - DatabaseConnectionException, DatabaseNotFoundException, ImageNotSupportedException, - PaginationException, FileStorageException, TableMalformedException, ContainerNotFoundException; +// /** +// * Exports a table to a file by given database and table id for a specific point in time. +// * +// * @param containerId The container id. +// * @param databaseId The database id. +// * @param tableId The table id. +// * @param timestamp The point in time. +// * @return The export. +// * @throws TableNotFoundException The table was not found. +// * @throws DatabaseConnectionException The connection to the database failed. +// * @throws DatabaseNotFoundException The database was not found. +// * @throws ImageNotSupportedException The image is not supported. Currently only MariaDB is supported. +// * @throws PaginationException The pagination failed. +// * @throws FileStorageException The table could not be exported. +// * @throws TableMalformedException The table is malformed. +// */ +// InputStreamResource export(Long containerId, Long databaseId, Long tableId, Instant timestamp) throws TableNotFoundException, +// DatabaseConnectionException, DatabaseNotFoundException, ImageNotSupportedException, +// PaginationException, FileStorageException, TableMalformedException, ContainerNotFoundException; - /** - * Exports a table to a file by given database and table id for a specific point in time. - * - * @param containerId The container id. - * @param databaseId The database id. - * @param tableId The table id. - * @return The export. - * @throws TableNotFoundException The table was not found. - * @throws DatabaseConnectionException The connection to the database failed. - * @throws DatabaseNotFoundException The database was not found. - * @throws ImageNotSupportedException The image is not supported. Currently only MariaDB is supported. - * @throws PaginationException The pagination failed. - * @throws FileStorageException The table could not be exported. - * @throws TableMalformedException The table is malformed. - */ - InputStreamResource export(Long containerId, Long databaseId, Long tableId) throws TableNotFoundException, DatabaseConnectionException, - TableMalformedException, DatabaseNotFoundException, ImageNotSupportedException, PaginationException, FileStorageException, ContainerNotFoundException; +// /** +// * Exports a table to a file by given database and table id for a specific point in time. +// * +// * @param containerId The container id. +// * @param databaseId The database id. +// * @param tableId The table id. +// * @return The export. +// * @throws TableNotFoundException The table was not found. +// * @throws DatabaseConnectionException The connection to the database failed. +// * @throws DatabaseNotFoundException The database was not found. +// * @throws ImageNotSupportedException The image is not supported. Currently only MariaDB is supported. +// * @throws PaginationException The pagination failed. +// * @throws FileStorageException The table could not be exported. +// * @throws TableMalformedException The table is malformed. +// */ +// InputStreamResource export(Long containerId, Long databaseId, Long tableId) throws TableNotFoundException, DatabaseConnectionException, +// TableMalformedException, DatabaseNotFoundException, ImageNotSupportedException, PaginationException, FileStorageException, ContainerNotFoundException; } diff --git a/fda-query-service/services/src/main/java/at/tuwien/service/QueryService.java b/fda-query-service/services/src/main/java/at/tuwien/service/QueryService.java index 2c75e1d2fa904cb6845dce490fa2e7974c23d3a8..b5e14e0bb25204a178d3a7db9599bafe59cf865d 100644 --- a/fda-query-service/services/src/main/java/at/tuwien/service/QueryService.java +++ b/fda-query-service/services/src/main/java/at/tuwien/service/QueryService.java @@ -5,8 +5,8 @@ import at.tuwien.api.database.query.QueryResultDto; import at.tuwien.api.database.table.TableCsvDto; import at.tuwien.exception.*; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; +import java.math.BigInteger; import java.time.Instant; @Service @@ -17,7 +17,6 @@ public interface QueryService { * default "mariadb" user is allowed read-only access "SELECT". * * @param databaseId The database id. - * @param tableId The table id. * @param query The query. * @return The result. * @throws TableNotFoundException @@ -26,7 +25,7 @@ public interface QueryService { * @throws DatabaseNotFoundException * @throws ImageNotSupportedException */ - QueryResultDto execute(Long containerId, Long databaseId, Long tableId, ExecuteStatementDto query) throws TableNotFoundException, + QueryResultDto execute(Long containerId, Long databaseId, ExecuteStatementDto query) throws TableNotFoundException, QueryStoreException, QueryMalformedException, DatabaseNotFoundException, ImageNotSupportedException, ContainerNotFoundException; /** @@ -57,7 +56,7 @@ public interface QueryService { * * @param containerId The container-database id tuple. * @param databaseId The container-database id tuple. - * @param tableId The table id. + * @param tableId The container-database id tuple. * @param timestamp The time. * @return The number of records, if successful * @throws ContainerNotFoundException The container was not found in the metadata database. @@ -66,7 +65,7 @@ public interface QueryService { * @throws TableMalformedException The table columns are messed up what we got from the metadata database. * @throws ImageNotSupportedException The image is not supported. */ - QueryResultDto count(Long containerId, Long databaseId, Long tableId, Instant timestamp) + BigInteger count(Long containerId, Long databaseId, Long tableId, Instant timestamp) throws ContainerNotFoundException, DatabaseNotFoundException, TableNotFoundException, TableMalformedException, ImageNotSupportedException; diff --git a/fda-query-service/services/src/main/java/at/tuwien/service/impl/CommaValueServiceImpl.java b/fda-query-service/services/src/main/java/at/tuwien/service/impl/CommaValueServiceImpl.java index b11e9c386e94eccc117d0864453e1a39e4cfad5b..aa225abfaa83ae51897d6f1f631375539decbd4c 100644 --- a/fda-query-service/services/src/main/java/at/tuwien/service/impl/CommaValueServiceImpl.java +++ b/fda-query-service/services/src/main/java/at/tuwien/service/impl/CommaValueServiceImpl.java @@ -1,6 +1,5 @@ package at.tuwien.service.impl; -import at.tuwien.api.database.query.QueryResultDto; import at.tuwien.api.database.table.TableCsvDto; import at.tuwien.entities.container.Container; import at.tuwien.entities.database.table.Table; @@ -8,7 +7,6 @@ import at.tuwien.exception.*; import at.tuwien.mapper.DataMapper; import at.tuwien.service.CommaValueService; import at.tuwien.service.ContainerService; -import at.tuwien.service.QueryService; import at.tuwien.service.TableService; import at.tuwien.utils.FileUtils; import at.tuwien.utils.TableUtils; @@ -20,8 +18,6 @@ import com.opencsv.exceptions.CsvException; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ClassPathResource; -import org.springframework.core.io.InputStreamResource; -import org.springframework.core.io.Resource; import org.springframework.mock.web.MockMultipartFile; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -31,7 +27,6 @@ import java.io.*; import java.net.URI; import java.nio.file.Files; import java.nio.file.Paths; -import java.time.Instant; import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -41,15 +36,12 @@ import java.util.stream.Stream; public class CommaValueServiceImpl implements CommaValueService { private final DataMapper dataMapper; - private final QueryService queryService; private final TableService tableService; private final ContainerService containerService; @Autowired - public CommaValueServiceImpl(DataMapper dataMapper, QueryService queryService, TableService tableService, - ContainerService containerService) { + public CommaValueServiceImpl(DataMapper dataMapper, TableService tableService, ContainerService containerService) { this.dataMapper = dataMapper; - this.queryService = queryService; this.tableService = tableService; this.containerService = containerService; } @@ -180,35 +172,35 @@ public class CommaValueServiceImpl implements CommaValueService { .build(); } - @Override - @Transactional - public InputStreamResource export(Long containerId, Long databaseId, Long tableId, Instant timestamp) - throws TableNotFoundException, DatabaseNotFoundException, DatabaseConnectionException, - TableMalformedException, ImageNotSupportedException, PaginationException, FileStorageException, - ContainerNotFoundException { - /* find */ - final Container container = containerService.find(containerId); - final Table table = tableService.find(databaseId, tableId); - final QueryResultDto result = queryService.findAll(containerId, databaseId, tableId, timestamp, null, null); - /* write */ - final Resource csv = dataMapper.resultTableToResource(result, table); - final InputStreamResource resource; - try { - resource = new InputStreamResource(csv.getInputStream()); - } catch (IOException e) { - log.error("Failed to map resource"); - throw new FileStorageException("Failed to map resource", e); - } - log.trace("produced csv {}", csv); - return resource; - } +// @Override +// @Transactional +// public InputStreamResource export(Long containerId, Long databaseId, Long tableId, Instant timestamp) +// throws TableNotFoundException, DatabaseNotFoundException, DatabaseConnectionException, +// TableMalformedException, ImageNotSupportedException, PaginationException, FileStorageException, +// ContainerNotFoundException { +// /* find */ +// final Container container = containerService.find(containerId); +// final Table table = tableService.find(databaseId, tableId); +// final QueryResultDto result = queryService.findAll(containerId, databaseId, tableId, timestamp, null, null); +// /* write */ +// final Resource csv = dataMapper.resultTableToResource(result, table); +// final InputStreamResource resource; +// try { +// resource = new InputStreamResource(csv.getInputStream()); +// } catch (IOException e) { +// log.error("Failed to map resource"); +// throw new FileStorageException("Failed to map resource", e); +// } +// log.trace("produced csv {}", csv); +// return resource; +// } - @Override - @Transactional - public InputStreamResource export(Long containerId, Long databaseId, Long tableId) throws TableNotFoundException, - DatabaseConnectionException, TableMalformedException, DatabaseNotFoundException, ImageNotSupportedException, - FileStorageException, PaginationException, ContainerNotFoundException { - return export(containerId, databaseId, tableId, Instant.now()); - } +// @Override +// @Transactional +// public InputStreamResource export(Long containerId, Long databaseId, Long tableId) throws TableNotFoundException, +// DatabaseConnectionException, TableMalformedException, DatabaseNotFoundException, ImageNotSupportedException, +// FileStorageException, PaginationException, ContainerNotFoundException { +// return export(containerId, databaseId, tableId, Instant.now()); +// } } diff --git a/fda-query-service/services/src/main/java/at/tuwien/service/impl/QueryServiceImpl.java b/fda-query-service/services/src/main/java/at/tuwien/service/impl/QueryServiceImpl.java index bfcd0750b4172ddc465ca251c43ee9039cf82c3b..958b92858b145ad248d02b83838b02611fda1725 100644 --- a/fda-query-service/services/src/main/java/at/tuwien/service/impl/QueryServiceImpl.java +++ b/fda-query-service/services/src/main/java/at/tuwien/service/impl/QueryServiceImpl.java @@ -4,11 +4,12 @@ import at.tuwien.InsertTableRawQuery; import at.tuwien.api.database.query.ExecuteStatementDto; import at.tuwien.api.database.query.QueryResultDto; import at.tuwien.api.database.table.TableCsvDto; -import at.tuwien.entities.container.Container; import at.tuwien.entities.database.Database; import at.tuwien.entities.database.table.Table; +import at.tuwien.entities.database.table.columns.TableColumn; import at.tuwien.exception.*; import at.tuwien.mapper.QueryMapper; +import at.tuwien.repository.jpa.TableColumnRepository; import at.tuwien.service.*; import lombok.extern.log4j.Log4j2; import org.hibernate.Session; @@ -23,6 +24,9 @@ import javax.persistence.PersistenceException; import java.math.BigInteger; import java.time.DateTimeException; import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; @Log4j2 @Service @@ -32,32 +36,30 @@ public class QueryServiceImpl extends HibernateConnector implements QueryService private final StoreService storeService; private final TableService tableService; private final DatabaseService databaseService; - private final ContainerService containerService; + private final TableColumnRepository tableColumnRepository; @Autowired public QueryServiceImpl(QueryMapper queryMapper, StoreService storeService, TableService tableService, - DatabaseService databaseService, ContainerService containerService) { + DatabaseService databaseService, TableColumnRepository tableColumnRepository) { this.queryMapper = queryMapper; this.storeService = storeService; this.tableService = tableService; this.databaseService = databaseService; - this.containerService = containerService; + this.tableColumnRepository = tableColumnRepository; } @Override @Transactional - public QueryResultDto execute(Long containerId, Long databaseId, Long tableId, ExecuteStatementDto statement) - throws DatabaseNotFoundException, ImageNotSupportedException, QueryMalformedException, - TableNotFoundException, ContainerNotFoundException { + public QueryResultDto execute(Long containerId, Long databaseId, ExecuteStatementDto statement) + throws DatabaseNotFoundException, ImageNotSupportedException, QueryMalformedException { /* find */ - final Container container = containerService.find(containerId); - final Table table = tableService.find(databaseId, tableId); - if (!table.getDatabase().getContainer().getImage().getRepository().equals("mariadb")) { + final Database database = databaseService.find(databaseId); + if (!database.getContainer().getImage().getRepository().equals("mariadb")) { throw new ImageNotSupportedException("Currently only MariaDB is supported"); } /* run query */ final long startSession = System.currentTimeMillis(); - final SessionFactory factory = getSessionFactory(table.getDatabase()); + final SessionFactory factory = getSessionFactory(database); final Session session = factory.openSession(); log.debug("opened hibernate session in {} ms", System.currentTimeMillis() - startSession); session.beginTransaction(); @@ -67,7 +69,7 @@ public class QueryServiceImpl extends HibernateConnector implements QueryService try { log.debug("execute raw view-only query {}", statement); affectedTuples = query.executeUpdate(); - log.info("Execution on table id {} affected {} rows", tableId, affectedTuples); + log.info("Execution on database id {} affected {} rows", databaseId, affectedTuples); session.getTransaction() .commit(); } catch (SQLGrammarException e) { @@ -75,7 +77,9 @@ public class QueryServiceImpl extends HibernateConnector implements QueryService factory.close(); throw new QueryMalformedException("Query not valid for this database", e); } - final QueryResultDto result = queryMapper.resultListToQueryResultDto(table, query.getResultList(), statement); + /* map the result to the tables (with respective columns) from the statement metadata */ + final List<TableColumn> columns = parseColumns(statement); + final QueryResultDto result = queryMapper.resultListToQueryResultDto(columns, query.getResultList()); session.close(); factory.close(); log.debug("query id {}", result.getId()); @@ -90,12 +94,11 @@ public class QueryServiceImpl extends HibernateConnector implements QueryService ImageNotSupportedException, DatabaseConnectionException, TableMalformedException, PaginationException, ContainerNotFoundException { /* find */ - final Container container = containerService.find(containerId); final Database database = databaseService.find(databaseId); final Table table = tableService.find(databaseId, tableId); /* run query */ final long startSession = System.currentTimeMillis(); - final SessionFactory factory = getSessionFactory(table.getDatabase(), true); + final SessionFactory factory = getSessionFactory(database, true); final Session session = factory.openSession(); log.debug("opened hibernate session in {} ms", System.currentTimeMillis() - startSession); session.beginTransaction(); @@ -104,7 +107,7 @@ public class QueryServiceImpl extends HibernateConnector implements QueryService final int affectedTuples; try { affectedTuples = query.executeUpdate(); - log.info("Found {} tuples in table id {}", affectedTuples, tableId); + log.info("Found {} tuples in database id {}", affectedTuples, databaseId); } catch (PersistenceException e) { log.error("Failed to find data"); session.close(); @@ -127,16 +130,15 @@ public class QueryServiceImpl extends HibernateConnector implements QueryService @Override @Transactional - public QueryResultDto count(Long containerId, Long databaseId, Long tableId, Instant timestamp) - throws ContainerNotFoundException, DatabaseNotFoundException, TableNotFoundException, + public BigInteger count(Long containerId, Long databaseId, Long tableId, Instant timestamp) + throws DatabaseNotFoundException, TableNotFoundException, TableMalformedException, ImageNotSupportedException { /* find */ - final Container container = containerService.find(containerId); final Database database = databaseService.find(databaseId); final Table table = tableService.find(databaseId, tableId); /* run query */ final long startSession = System.currentTimeMillis(); - final SessionFactory factory = getSessionFactory(table.getDatabase(), false); + final SessionFactory factory = getSessionFactory(database, false); final Session session = factory.openSession(); log.debug("opened hibernate session in {} ms", System.currentTimeMillis() - startSession); session.beginTransaction(); @@ -153,12 +155,10 @@ public class QueryServiceImpl extends HibernateConnector implements QueryService } session.getTransaction() .commit(); - final QueryResultDto result = QueryResultDto.builder() - .count(query.getSingleResult()) - .build(); + final BigInteger count = query.getSingleResult(); session.close(); factory.close(); - return result; + return count; } @Override @@ -167,13 +167,12 @@ public class QueryServiceImpl extends HibernateConnector implements QueryService throws ImageNotSupportedException, TableMalformedException, DatabaseNotFoundException, TableNotFoundException, ContainerNotFoundException { /* find */ - final Container container = containerService.find(containerId); final Database database = databaseService.find(databaseId); final Table table = tableService.find(databaseId, tableId); /* run query */ if (data.getData().size() == 0) return null; final long startSession = System.currentTimeMillis(); - final SessionFactory factory = getSessionFactory(table.getDatabase(), true); + final SessionFactory factory = getSessionFactory(database, true); final Session session = factory.openSession(); log.debug("opened hibernate session in {} ms", System.currentTimeMillis() - startSession); session.beginTransaction(); @@ -181,7 +180,7 @@ public class QueryServiceImpl extends HibernateConnector implements QueryService final InsertTableRawQuery raw = queryMapper.tableCsvDtoToRawInsertQuery(table, data); final NativeQuery<?> query = session.createSQLQuery(raw.getQuery()); log.trace("query with parameters {}", query.setParameterList(1, raw.getData())); - final Integer affected = insert(query, session, factory, tableId); + final Integer affected = insert(query, session, factory); storeService.createVersion(containerId, databaseId); return affected; } @@ -192,19 +191,18 @@ public class QueryServiceImpl extends HibernateConnector implements QueryService throws ImageNotSupportedException, TableMalformedException, DatabaseNotFoundException, TableNotFoundException, ContainerNotFoundException { /* find */ - final Container container = containerService.find(containerId); final Database database = databaseService.find(databaseId); final Table table = tableService.find(databaseId, tableId); /* run query */ final long startSession = System.currentTimeMillis(); - final SessionFactory factory = getSessionFactory(table.getDatabase(), true); + final SessionFactory factory = getSessionFactory(database, true); final Session session = factory.openSession(); log.debug("opened hibernate session in {} ms", System.currentTimeMillis() - startSession); session.beginTransaction(); /* prepare the statement */ final InsertTableRawQuery raw = queryMapper.pathToRawInsertQuery(table, path); final NativeQuery<?> query = session.createSQLQuery(raw.getQuery()); - final Integer affected = insert(query, session, factory, tableId); + final Integer affected = insert(query, session, factory); storeService.createVersion(containerId, databaseId); return affected; } @@ -215,15 +213,13 @@ public class QueryServiceImpl extends HibernateConnector implements QueryService * @param query The query. * @param session The active Hibernate session. * @param factory The active Hibernate session factory. - * @param tableId The table id. * @return The affected rows, if successful. * @throws TableMalformedException The table metadata is wrong. */ - private Integer insert(NativeQuery<?> query, Session session, SessionFactory factory, Long tableId) throws TableMalformedException { + private Integer insert(NativeQuery<?> query, Session session, SessionFactory factory) throws TableMalformedException { final int affectedTuples; try { affectedTuples = query.executeUpdate(); - log.info("Inserted {} tuples on table id {}", affectedTuples, tableId); } catch (PersistenceException e) { log.error("Could not insert data"); session.close(); @@ -237,4 +233,19 @@ public class QueryServiceImpl extends HibernateConnector implements QueryService return affectedTuples; } + /** + * Retrieves the columns from the tables (ids) and referenced column ids from the metadata database + * + * @param statement The list of tables (ids) and referenced column ids. + * @return The list of columns if successful + */ + private List<TableColumn> parseColumns(ExecuteStatementDto statement) { + return statement.getColumns() + .stream() + .map(c -> tableColumnRepository.findById(c.getId())) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + } + } diff --git a/fda-table-service/pom.xml b/fda-table-service/pom.xml index bac6894b3c2fe019881eda35b4bb75a486614912..c438df0222d945aae4bd61ded57b955ce033aecf 100644 --- a/fda-table-service/pom.xml +++ b/fda-table-service/pom.xml @@ -56,6 +56,15 @@ <artifactId>spring-cloud-starter-bootstrap</artifactId> <version>${spring-cloud.version}</version> </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-security</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.security</groupId> + <artifactId>spring-security-test</artifactId> + <scope>test</scope> + </dependency> <!-- Entities and API --> <dependency> <groupId>at.tuwien</groupId> diff --git a/fda-table-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java b/fda-table-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java index 7c4870663a2de27e2378d50bae620e3a40efdadc..78f9cb54d6d5526f6a392f1a7a5a52ef1808d836 100644 --- a/fda-table-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java +++ b/fda-table-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java @@ -15,6 +15,7 @@ import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; @@ -40,7 +41,6 @@ public class TableEndpoint { this.tableMapper = tableMapper; } - @Transactional @GetMapping @ApiOperation(value = "List all tables", notes = "Lists the tables in the metadata database for this database.") @ApiResponses({ @@ -56,8 +56,8 @@ public class TableEndpoint { .collect(Collectors.toList())); } - @Transactional @PostMapping + @PreAuthorize("hasRole('ROLE_RESEARCHER')") @ApiOperation(value = "Create a table", notes = "Creates a new table for a database, requires a running container.") @ApiResponses({ @ApiResponse(code = 201, message = "The table was created."), @@ -80,7 +80,6 @@ public class TableEndpoint { } - @Transactional @GetMapping("/{tableId}") @ApiOperation(value = "Get information about table", notes = "Lists the information of a table from the metadata database for this database.") @ApiResponses({ @@ -112,6 +111,7 @@ public class TableEndpoint { } @DeleteMapping("/{tableId}") + @PreAuthorize("hasRole('ROLE_DEVELOPER') or hasRole('ROLE_DATA_STEWARD')") @ApiOperation(value = "Delete a table", notes = "Delete a table in the database.") @ApiResponses({ @ApiResponse(code = 200, message = "Deleted the table."), diff --git a/fda-table-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java b/fda-table-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..6bcb362bcd7ed5d338c250782c68145597fce5df --- /dev/null +++ b/fda-table-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java @@ -0,0 +1,56 @@ +package at.tuwien.auth; + +import at.tuwien.gateway.AuthenticationServiceGateway; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Slf4j +public class AuthTokenFilter extends OncePerRequestFilter { + + private final AuthenticationServiceGateway authenticationServiceGateway; + + public AuthTokenFilter(AuthenticationServiceGateway authenticationServiceGateway) { + this.authenticationServiceGateway = authenticationServiceGateway; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + String jwt = parseJwt(request); + log.debug("parsed jwt {}", jwt); + if (jwt != null) { + final UserDetails userDetails = authenticationServiceGateway.validate(jwt); + final UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + filterChain.doFilter(request, response); + } + + /** + * Parses the token from the HTTP header of the request + * + * @param request The request. + * @return The token. + */ + private String parseJwt(HttpServletRequest request) { + String headerAuth = request.getHeader("Authorization"); + if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) { + return headerAuth.substring(7, headerAuth.length()); + } + return null; + } +} \ No newline at end of file diff --git a/fda-table-service/services/src/main/java/at/tuwien/config/GatewayConfig.java b/fda-table-service/services/src/main/java/at/tuwien/config/GatewayConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..252235a1eb97fcee790d7cf270935f06924f007f --- /dev/null +++ b/fda-table-service/services/src/main/java/at/tuwien/config/GatewayConfig.java @@ -0,0 +1,22 @@ +package at.tuwien.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.DefaultUriBuilderFactory; + +@Configuration +public class GatewayConfig { + + @Value("${fda.auth.endpoint}") + private String authEndpoint; + + @Bean + public RestTemplate restTemplate() { + final RestTemplate restTemplate = new RestTemplate(); + restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(authEndpoint)); + return restTemplate; + } + +} diff --git a/fda-table-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java b/fda-table-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..4fbc0cf4c291754bfefc6102ed0de24e244ea337 --- /dev/null +++ b/fda-table-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java @@ -0,0 +1,81 @@ +package at.tuwien.config; + +import at.tuwien.auth.AuthTokenFilter; +import at.tuwien.gateway.AuthenticationServiceGateway; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import javax.servlet.http.HttpServletResponse; + +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + + private final AuthenticationServiceGateway authenticationServiceGateway; + + @Autowired + public WebSecurityConfig(AuthenticationServiceGateway authenticationServiceGateway) { + this.authenticationServiceGateway = authenticationServiceGateway; + } + + @Bean + public AuthTokenFilter authTokenFilter() { + return new AuthTokenFilter(authenticationServiceGateway); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + /* enable CORS and disable CSRF */ + http = http.cors().and().csrf().disable(); + /* set session management to stateless */ + http = http + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and(); + /* set unauthorized requests exception handler */ + http = http + .exceptionHandling() + .authenticationEntryPoint( + (request, response, ex) -> { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, + ex.getMessage() + ); + } + ).and(); + /* set permissions on endpoints */ + http.authorizeRequests() + /* our public endpoints */ + .antMatchers(HttpMethod.GET, "/api/container/**/database/**/table/**").permitAll() + /* our private endpoints */ + .anyRequest().authenticated(); + /* add JWT token filter */ + http.addFilterBefore(authTokenFilter(), + UsernamePasswordAuthenticationFilter.class + ); + } + + @Bean + public CorsFilter corsFilter() { + final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + final CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + config.addAllowedOrigin("*"); + config.addAllowedHeader("*"); + config.addAllowedMethod("*"); + source.registerCorsConfiguration("/**", config); + return new CorsFilter(source); + } + +} diff --git a/fda-table-service/services/src/main/java/at/tuwien/gateway/AuthenticationServiceGateway.java b/fda-table-service/services/src/main/java/at/tuwien/gateway/AuthenticationServiceGateway.java new file mode 100644 index 0000000000000000000000000000000000000000..fc47ce246da851244cf1630b15f493ded20249d8 --- /dev/null +++ b/fda-table-service/services/src/main/java/at/tuwien/gateway/AuthenticationServiceGateway.java @@ -0,0 +1,14 @@ +package at.tuwien.gateway; + +import org.springframework.security.core.userdetails.UserDetails; + +public interface AuthenticationServiceGateway { + + /** + * Validates a token + * + * @param token The token + * @return User details on success + */ + UserDetails validate(String token); +} diff --git a/fda-table-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java b/fda-table-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..30507c4b6e662a7e772e85c609fdb5fbc563a99f --- /dev/null +++ b/fda-table-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java @@ -0,0 +1,36 @@ +package at.tuwien.gateway.impl; + +import at.tuwien.api.user.UserDto; +import at.tuwien.gateway.AuthenticationServiceGateway; +import at.tuwien.mapper.UserMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Service +public class AuthenticationServiceGatewayImpl implements AuthenticationServiceGateway { + + private final UserMapper userMapper; + private final RestTemplate restTemplate; + + @Autowired + public AuthenticationServiceGatewayImpl(UserMapper userMapper, RestTemplate restTemplate) { + this.userMapper = userMapper; + this.restTemplate = restTemplate; + } + + @Override + public UserDetails validate(String token) { + final HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "Bearer " + token); + final ResponseEntity<UserDto> response = restTemplate.exchange("/api/auth", HttpMethod.PUT, + new HttpEntity<>("", headers), UserDto.class); + return userMapper.userDtoToUserDetailsDto(response.getBody()); + } + +} diff --git a/fda-table-service/services/src/main/java/at/tuwien/mapper/UserMapper.java b/fda-table-service/services/src/main/java/at/tuwien/mapper/UserMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..f14430e9675c7a152c778df18fc892e3423afdc6 --- /dev/null +++ b/fda-table-service/services/src/main/java/at/tuwien/mapper/UserMapper.java @@ -0,0 +1,18 @@ +package at.tuwien.mapper; + +import at.tuwien.api.user.GrantedAuthorityDto; +import at.tuwien.api.user.UserDetailsDto; +import at.tuwien.api.user.UserDto; +import org.mapstruct.Mapper; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +@Mapper(componentModel = "spring") +public interface UserMapper { + + UserDetailsDto userDtoToUserDetailsDto(UserDto data); + + default GrantedAuthority grantedAuthorityDtoToGrantedAuthority(GrantedAuthorityDto data) { + return new SimpleGrantedAuthority(data.getAuthority()); + } +}