From c3f990319670825602c6decf45e329aaae917d7e Mon Sep 17 00:00:00 2001 From: Yasin Date: Thu, 21 Nov 2024 14:23:27 +0100 Subject: [PATCH 01/11] feat: add export req. controller, service, and configs --- .../fega/ltp/aspects/PublishMQAspect.java | 12 +-- .../fega/ltp/config/RabbitMQConfig.java | 34 +++++++++ .../rest/ExportRequestController.java | 73 +++++++++++++++++++ .../no/elixir/fega/ltp/dto/ExportRequest.java | 50 +++++++++++++ .../elixir/fega/ltp/dto/GenericResponse.java | 14 ++++ .../ltp/services/ExportRequestService.java | 42 +++++++++++ .../src/main/resources/application.yaml | 44 +++++++---- 7 files changed, 248 insertions(+), 21 deletions(-) create mode 100644 services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/config/RabbitMQConfig.java create mode 100644 services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/controllers/rest/ExportRequestController.java create mode 100644 services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/dto/ExportRequest.java create mode 100644 services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/dto/GenericResponse.java create mode 100644 services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/services/ExportRequestService.java diff --git a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/aspects/PublishMQAspect.java b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/aspects/PublishMQAspect.java index 7a604865..518afd9d 100644 --- a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/aspects/PublishMQAspect.java +++ b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/aspects/PublishMQAspect.java @@ -32,7 +32,7 @@ public class PublishMQAspect { private final Gson gson; - private final RabbitTemplate rabbitTemplate; + private final RabbitTemplate cegaRabbitTemplate; @Value("${tsd.project}") private String tsdProjectId; @@ -40,17 +40,17 @@ public class PublishMQAspect { @Value("${tsd.inbox-location}") private String tsdInboxLocation; - @Value("${mq.exchange}") + @Value("${mq.cega.exchange}") private String exchange; - @Value("${mq.routing-key}") + @Value("${mq.cega.routing-key}") private String routingKey; @Autowired - public PublishMQAspect(HttpServletRequest request, Gson gson, RabbitTemplate rabbitTemplate) { + public PublishMQAspect(HttpServletRequest request, Gson gson, RabbitTemplate cegaRabbitTemplate) { this.request = request; this.gson = gson; - this.rabbitTemplate = rabbitTemplate; + this.cegaRabbitTemplate = cegaRabbitTemplate; } /** @@ -139,7 +139,7 @@ public void publishRemove(Object result) { private void publishMessage(FileDescriptor fileDescriptor) { String json = gson.toJson(fileDescriptor); - rabbitTemplate.convertAndSend( + cegaRabbitTemplate.convertAndSend( exchange, routingKey, json, diff --git a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/config/RabbitMQConfig.java b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/config/RabbitMQConfig.java new file mode 100644 index 00000000..462086ba --- /dev/null +++ b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/config/RabbitMQConfig.java @@ -0,0 +1,34 @@ +package no.elixir.fega.ltp.config; + +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RabbitMQConfig { + + @Bean + @ConfigurationProperties(prefix = "spring.rabbitmq.connections.cega") + public CachingConnectionFactory cegaConnectionFactory() { + return new CachingConnectionFactory(); + } + + @Bean + public RabbitTemplate cegaRabbitTemplate(CachingConnectionFactory cegaConnectionFactory) { + return new RabbitTemplate(cegaConnectionFactory); + } + + @Bean + @ConfigurationProperties(prefix = "spring.rabbitmq.connections.tsd") + public CachingConnectionFactory tsdConnectionFactory() { + return new CachingConnectionFactory(); + } + + @Bean + public RabbitTemplate tsdRabbitTemplate(CachingConnectionFactory tsdConnectionFactory) { + return new RabbitTemplate(tsdConnectionFactory); + } + +} diff --git a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/controllers/rest/ExportRequestController.java b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/controllers/rest/ExportRequestController.java new file mode 100644 index 00000000..dffc8e6c --- /dev/null +++ b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/controllers/rest/ExportRequestController.java @@ -0,0 +1,73 @@ +package no.elixir.fega.ltp.controllers.rest; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import no.elixir.fega.ltp.dto.ExportRequest; +import no.elixir.fega.ltp.dto.GenericResponse; +import no.elixir.fega.ltp.services.ExportRequestService; +import no.elixir.fega.ltp.services.TokenService; +import no.uio.ifi.clearinghouse.model.Visa; +import no.uio.ifi.clearinghouse.model.VisaType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@Slf4j +@RestController +public class ExportRequestController { + + private final TokenService tokenService; + private final ExportRequestService exportRequestService; + + @Autowired + public ExportRequestController(TokenService tokenService, ExportRequestService exportRequestService) { + this.tokenService = tokenService; + this.exportRequestService = exportRequestService; + } + + @PostMapping("/export") + public ResponseEntity exportRequest( + HttpServletRequest request, @RequestBody @NotNull ExportRequest body) { + + String bearerAuth = request.getHeader(HttpHeaders.PROXY_AUTHORIZATION); + if (bearerAuth == null || bearerAuth.isEmpty()) { + log.info("Authentication attempt without Elixir AAI access token provided"); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + + String accessToken = bearerAuth.replace("Bearer ", ""); + + try { + String subject = tokenService.getSubject(accessToken); + List controlledAccessGrantsVisas = + tokenService.filterByVisaType( + tokenService.fetchTheFullPassportUsingPassportScopedAccessTokenAndGetVisas( + accessToken), + VisaType.ControlledAccessGrants); + log.info( + "Elixir user {} authenticated and provided following valid GA4GH Visas: {}", + subject, + controlledAccessGrantsVisas); + } catch (Exception e) { + log.info(e.getMessage(), e); + return ResponseEntity + .status(HttpStatus.FORBIDDEN) + .body(new GenericResponse(e.getMessage())); + } + + exportRequestService.exportRequest(body); + + return ResponseEntity + .status(HttpStatus.OK) + .body(new GenericResponse("Export request completed successfully")); + + } + +} diff --git a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/dto/ExportRequest.java b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/dto/ExportRequest.java new file mode 100644 index 00000000..5744e8bc --- /dev/null +++ b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/dto/ExportRequest.java @@ -0,0 +1,50 @@ +package no.elixir.fega.ltp.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.JsonObject; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Data +@ToString +@AllArgsConstructor +@NoArgsConstructor +public class ExportRequest { + + @NotBlank(message = "The field 'jwtToken' must not be blank.") + @JsonProperty + private String jwtToken; + + @NotBlank(message = "The field 'id' must not be blank.") + @JsonProperty + private String id; + + @NotBlank(message = "The field 'userPublicKey' must not be blank.") + @JsonProperty + private String userPublicKey; + + @NotNull(message = "The field 'type' must not be null. Should be either 'fileId' or 'datasetId'.") + @JsonProperty + private ExportType type = ExportType.DATASET_ID; + + public String toJson() { + JsonObject obj = new JsonObject(); + obj.addProperty("jwtToken", jwtToken); + obj.addProperty(type.value, id); + obj.addProperty("userPublicKey", userPublicKey); + return obj.getAsString(); + } + + @ToString + @AllArgsConstructor + public enum ExportType { + FILE_ID("fileId"), + DATASET_ID("datasetId"); + private final String value; + } + +} diff --git a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/dto/GenericResponse.java b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/dto/GenericResponse.java new file mode 100644 index 00000000..93155fc1 --- /dev/null +++ b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/dto/GenericResponse.java @@ -0,0 +1,14 @@ +package no.elixir.fega.ltp.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class GenericResponse { + + private String message; + +} diff --git a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/services/ExportRequestService.java b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/services/ExportRequestService.java new file mode 100644 index 00000000..385f70dd --- /dev/null +++ b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/services/ExportRequestService.java @@ -0,0 +1,42 @@ +package no.elixir.fega.ltp.services; + +import lombok.extern.slf4j.Slf4j; +import no.elixir.fega.ltp.dto.ExportRequest; +import org.apache.http.entity.ContentType; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.UUID; + +@Slf4j +@Service +public class ExportRequestService { + + private final RabbitTemplate tsdRabbitTemplate; + + @Value("${mq.tsd.exchange}") + private String exchange; + @Value("${mq.tsd.routing-key}") + private String routingKey; + + @Autowired + public ExportRequestService(RabbitTemplate tsdRabbitTemplate) { + this.tsdRabbitTemplate = tsdRabbitTemplate; + } + + public void exportRequest(ExportRequest exportRequest) { + log.info("Export request: {} | Exchange: {} | Routing-key: {}", exportRequest, exchange, routingKey); + tsdRabbitTemplate.convertAndSend( + exchange, + routingKey, + exportRequest.toJson(), + m -> { + m.getMessageProperties().setContentType(ContentType.APPLICATION_JSON.getMimeType()); + m.getMessageProperties().setCorrelationId(UUID.randomUUID().toString()); + return m; + }); + } + +} diff --git a/services/localega-tsd-proxy/src/main/resources/application.yaml b/services/localega-tsd-proxy/src/main/resources/application.yaml index 7aa77273..575cf219 100644 --- a/services/localega-tsd-proxy/src/main/resources/application.yaml +++ b/services/localega-tsd-proxy/src/main/resources/application.yaml @@ -5,11 +5,16 @@ server.ssl: key-store-password: ${SERVER_CERT_PASSWORD} spring: + + main: + allow-circular-references: true + datasource: url: jdbc:postgresql://${DB_INSTANCE:postgres}:${DB_PORT:5432}/${POSTGRES_DB:postgres} username: ${POSTGRES_USER:postgres} password: ${POSTGRES_PASSWORD} driver-class-name: org.postgresql.Driver + jpa: properties: hibernate: @@ -31,19 +36,24 @@ spring: cache: type: simple -spring.rabbitmq: - host: ${BROKER_HOST:public-mq} - port: ${BROKER_PORT:5671} - virtual-host: ${BROKER_VHOST:/} - username: ${BROKER_USERNAME:admin} - password: ${BROKER_PASSWORD:guest} - ssl: - validate-server-certificate: ${BROKER_VALIDATE:true} - enabled: ${BROKER_SSL_ENABLED:true} - algorithm: TLSv1.2 - -spring.main: - allow-circular-references: true + rabbitmq: + connections: + cega: + host: ${BROKER_HOST:public-mq} + port: ${BROKER_PORT:5671} + virtual-host: ${BROKER_VHOST:/} + username: ${BROKER_USERNAME:admin} + password: ${BROKER_PASSWORD:guest} + ssl: + validate-server-certificate: ${BROKER_VALIDATE:true} + enabled: ${BROKER_SSL_ENABLED:true} + algorithm: TLSv1.2 + tsd: + host: ${TSD_MQ_HOST:mq} + port: ${TSD_MQ_PORT:5671} + virtual-host: ${TSD_MQ_VHOST:/} + username: ${TSD_MQ_USERNAME:admin} + password: ${TSD_MQ_PASSWORD:admin} elixir: client: @@ -77,8 +87,12 @@ tsd: root-ca-password: ${TSD_ROOT_CERT_PASSWORD:} mq: - exchange: ${EXCHANGE:cega} - routing-key: ${ROUTING_KEY:files.inbox} + cega: + exchange: ${EXCHANGE:cega} + routing-key: ${ROUTING_KEY:files.inbox} + tsd: + exchange: ${TSD_MQ_EXCHANGE:sda} + routing-key: ${TSD_MQ_ROUTING_KEY:exportRequests} heartbeat: ok_if_ok_is_after_failed_and_diff_in_minutes_ge: 10 From e9e76e9e6f564919b3d1b5ff2ab76180b223d21b Mon Sep 17 00:00:00 2001 From: Yasin Date: Thu, 21 Nov 2024 14:24:03 +0100 Subject: [PATCH 02/11] feat: add definition for exportRequests queue --- e2eTests/confs/mq/definitions.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/e2eTests/confs/mq/definitions.json b/e2eTests/confs/mq/definitions.json index 2e935afb..4cac4cd7 100755 --- a/e2eTests/confs/mq/definitions.json +++ b/e2eTests/confs/mq/definitions.json @@ -97,6 +97,14 @@ "auto_delete": false, "arguments": { } + }, + { + "name": "exportRequests", + "vhost": "<>", + "durable": true, + "auto_delete": false, + "arguments": { + } } ], "exchanges": [ @@ -201,6 +209,15 @@ "routing_key": "files", "arguments": { } + }, + { + "source": "sda", + "vhost": "<>", + "destination": "exportRequests", + "destination_type": "queue", + "routing_key": "exportRequests", + "arguments": { + } } ] } \ No newline at end of file From f40bc45901a69382de9c04aa6b36c9d9d14c3abd Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Thu, 21 Nov 2024 13:30:18 +0000 Subject: [PATCH 03/11] Code formatting applied --- .../fega/ltp/config/RabbitMQConfig.java | 37 +++++---- .../rest/ExportRequestController.java | 79 +++++++++---------- .../no/elixir/fega/ltp/dto/ExportRequest.java | 62 +++++++-------- .../elixir/fega/ltp/dto/GenericResponse.java | 3 +- .../ltp/services/ExportRequestService.java | 54 ++++++------- 5 files changed, 113 insertions(+), 122 deletions(-) diff --git a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/config/RabbitMQConfig.java b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/config/RabbitMQConfig.java index 462086ba..6fa4b865 100644 --- a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/config/RabbitMQConfig.java +++ b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/config/RabbitMQConfig.java @@ -9,26 +9,25 @@ @Configuration public class RabbitMQConfig { - @Bean - @ConfigurationProperties(prefix = "spring.rabbitmq.connections.cega") - public CachingConnectionFactory cegaConnectionFactory() { - return new CachingConnectionFactory(); - } + @Bean + @ConfigurationProperties(prefix = "spring.rabbitmq.connections.cega") + public CachingConnectionFactory cegaConnectionFactory() { + return new CachingConnectionFactory(); + } - @Bean - public RabbitTemplate cegaRabbitTemplate(CachingConnectionFactory cegaConnectionFactory) { - return new RabbitTemplate(cegaConnectionFactory); - } + @Bean + public RabbitTemplate cegaRabbitTemplate(CachingConnectionFactory cegaConnectionFactory) { + return new RabbitTemplate(cegaConnectionFactory); + } - @Bean - @ConfigurationProperties(prefix = "spring.rabbitmq.connections.tsd") - public CachingConnectionFactory tsdConnectionFactory() { - return new CachingConnectionFactory(); - } - - @Bean - public RabbitTemplate tsdRabbitTemplate(CachingConnectionFactory tsdConnectionFactory) { - return new RabbitTemplate(tsdConnectionFactory); - } + @Bean + @ConfigurationProperties(prefix = "spring.rabbitmq.connections.tsd") + public CachingConnectionFactory tsdConnectionFactory() { + return new CachingConnectionFactory(); + } + @Bean + public RabbitTemplate tsdRabbitTemplate(CachingConnectionFactory tsdConnectionFactory) { + return new RabbitTemplate(tsdConnectionFactory); + } } diff --git a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/controllers/rest/ExportRequestController.java b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/controllers/rest/ExportRequestController.java index dffc8e6c..414fc2fa 100644 --- a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/controllers/rest/ExportRequestController.java +++ b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/controllers/rest/ExportRequestController.java @@ -2,6 +2,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.constraints.NotNull; +import java.util.List; import lombok.extern.slf4j.Slf4j; import no.elixir.fega.ltp.dto.ExportRequest; import no.elixir.fega.ltp.dto.GenericResponse; @@ -17,57 +18,51 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; -import java.util.List; - @Slf4j @RestController public class ExportRequestController { - private final TokenService tokenService; - private final ExportRequestService exportRequestService; - - @Autowired - public ExportRequestController(TokenService tokenService, ExportRequestService exportRequestService) { - this.tokenService = tokenService; - this.exportRequestService = exportRequestService; - } + private final TokenService tokenService; + private final ExportRequestService exportRequestService; - @PostMapping("/export") - public ResponseEntity exportRequest( - HttpServletRequest request, @RequestBody @NotNull ExportRequest body) { + @Autowired + public ExportRequestController( + TokenService tokenService, ExportRequestService exportRequestService) { + this.tokenService = tokenService; + this.exportRequestService = exportRequestService; + } - String bearerAuth = request.getHeader(HttpHeaders.PROXY_AUTHORIZATION); - if (bearerAuth == null || bearerAuth.isEmpty()) { - log.info("Authentication attempt without Elixir AAI access token provided"); - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } + @PostMapping("/export") + public ResponseEntity exportRequest( + HttpServletRequest request, @RequestBody @NotNull ExportRequest body) { - String accessToken = bearerAuth.replace("Bearer ", ""); - - try { - String subject = tokenService.getSubject(accessToken); - List controlledAccessGrantsVisas = - tokenService.filterByVisaType( - tokenService.fetchTheFullPassportUsingPassportScopedAccessTokenAndGetVisas( - accessToken), - VisaType.ControlledAccessGrants); - log.info( - "Elixir user {} authenticated and provided following valid GA4GH Visas: {}", - subject, - controlledAccessGrantsVisas); - } catch (Exception e) { - log.info(e.getMessage(), e); - return ResponseEntity - .status(HttpStatus.FORBIDDEN) - .body(new GenericResponse(e.getMessage())); - } - - exportRequestService.exportRequest(body); + String bearerAuth = request.getHeader(HttpHeaders.PROXY_AUTHORIZATION); + if (bearerAuth == null || bearerAuth.isEmpty()) { + log.info("Authentication attempt without Elixir AAI access token provided"); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } - return ResponseEntity - .status(HttpStatus.OK) - .body(new GenericResponse("Export request completed successfully")); + String accessToken = bearerAuth.replace("Bearer ", ""); + try { + String subject = tokenService.getSubject(accessToken); + List controlledAccessGrantsVisas = + tokenService.filterByVisaType( + tokenService.fetchTheFullPassportUsingPassportScopedAccessTokenAndGetVisas( + accessToken), + VisaType.ControlledAccessGrants); + log.info( + "Elixir user {} authenticated and provided following valid GA4GH Visas: {}", + subject, + controlledAccessGrantsVisas); + } catch (Exception e) { + log.info(e.getMessage(), e); + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new GenericResponse(e.getMessage())); } + exportRequestService.exportRequest(body); + + return ResponseEntity.status(HttpStatus.OK) + .body(new GenericResponse("Export request completed successfully")); + } } diff --git a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/dto/ExportRequest.java b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/dto/ExportRequest.java index 5744e8bc..4c95c250 100644 --- a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/dto/ExportRequest.java +++ b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/dto/ExportRequest.java @@ -15,36 +15,34 @@ @NoArgsConstructor public class ExportRequest { - @NotBlank(message = "The field 'jwtToken' must not be blank.") - @JsonProperty - private String jwtToken; - - @NotBlank(message = "The field 'id' must not be blank.") - @JsonProperty - private String id; - - @NotBlank(message = "The field 'userPublicKey' must not be blank.") - @JsonProperty - private String userPublicKey; - - @NotNull(message = "The field 'type' must not be null. Should be either 'fileId' or 'datasetId'.") - @JsonProperty - private ExportType type = ExportType.DATASET_ID; - - public String toJson() { - JsonObject obj = new JsonObject(); - obj.addProperty("jwtToken", jwtToken); - obj.addProperty(type.value, id); - obj.addProperty("userPublicKey", userPublicKey); - return obj.getAsString(); - } - - @ToString - @AllArgsConstructor - public enum ExportType { - FILE_ID("fileId"), - DATASET_ID("datasetId"); - private final String value; - } - + @NotBlank(message = "The field 'jwtToken' must not be blank.") + @JsonProperty + private String jwtToken; + + @NotBlank(message = "The field 'id' must not be blank.") + @JsonProperty + private String id; + + @NotBlank(message = "The field 'userPublicKey' must not be blank.") + @JsonProperty + private String userPublicKey; + + @NotNull(message = "The field 'type' must not be null. Should be either 'fileId' or 'datasetId'.") @JsonProperty + private ExportType type = ExportType.DATASET_ID; + + public String toJson() { + JsonObject obj = new JsonObject(); + obj.addProperty("jwtToken", jwtToken); + obj.addProperty(type.value, id); + obj.addProperty("userPublicKey", userPublicKey); + return obj.getAsString(); + } + + @ToString + @AllArgsConstructor + public enum ExportType { + FILE_ID("fileId"), + DATASET_ID("datasetId"); + private final String value; + } } diff --git a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/dto/GenericResponse.java b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/dto/GenericResponse.java index 93155fc1..599e3cda 100644 --- a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/dto/GenericResponse.java +++ b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/dto/GenericResponse.java @@ -9,6 +9,5 @@ @NoArgsConstructor public class GenericResponse { - private String message; - + private String message; } diff --git a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/services/ExportRequestService.java b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/services/ExportRequestService.java index 385f70dd..95fefe5a 100644 --- a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/services/ExportRequestService.java +++ b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/services/ExportRequestService.java @@ -1,5 +1,6 @@ package no.elixir.fega.ltp.services; +import java.util.UUID; import lombok.extern.slf4j.Slf4j; import no.elixir.fega.ltp.dto.ExportRequest; import org.apache.http.entity.ContentType; @@ -8,35 +9,34 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import java.util.UUID; - @Slf4j @Service public class ExportRequestService { - private final RabbitTemplate tsdRabbitTemplate; - - @Value("${mq.tsd.exchange}") - private String exchange; - @Value("${mq.tsd.routing-key}") - private String routingKey; - - @Autowired - public ExportRequestService(RabbitTemplate tsdRabbitTemplate) { - this.tsdRabbitTemplate = tsdRabbitTemplate; - } - - public void exportRequest(ExportRequest exportRequest) { - log.info("Export request: {} | Exchange: {} | Routing-key: {}", exportRequest, exchange, routingKey); - tsdRabbitTemplate.convertAndSend( - exchange, - routingKey, - exportRequest.toJson(), - m -> { - m.getMessageProperties().setContentType(ContentType.APPLICATION_JSON.getMimeType()); - m.getMessageProperties().setCorrelationId(UUID.randomUUID().toString()); - return m; - }); - } - + private final RabbitTemplate tsdRabbitTemplate; + + @Value("${mq.tsd.exchange}") + private String exchange; + + @Value("${mq.tsd.routing-key}") + private String routingKey; + + @Autowired + public ExportRequestService(RabbitTemplate tsdRabbitTemplate) { + this.tsdRabbitTemplate = tsdRabbitTemplate; + } + + public void exportRequest(ExportRequest exportRequest) { + log.info( + "Export request: {} | Exchange: {} | Routing-key: {}", exportRequest, exchange, routingKey); + tsdRabbitTemplate.convertAndSend( + exchange, + routingKey, + exportRequest.toJson(), + m -> { + m.getMessageProperties().setContentType(ContentType.APPLICATION_JSON.getMimeType()); + m.getMessageProperties().setCorrelationId(UUID.randomUUID().toString()); + return m; + }); + } } From 6a8125d630c7a431879fdbb72f29ffade038abd8 Mon Sep 17 00:00:00 2001 From: Yasin Date: Fri, 22 Nov 2024 15:22:58 +0100 Subject: [PATCH 04/11] fix: add EOF to definitions.json --- e2eTests/confs/mq/definitions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2eTests/confs/mq/definitions.json b/e2eTests/confs/mq/definitions.json index 4cac4cd7..10860266 100755 --- a/e2eTests/confs/mq/definitions.json +++ b/e2eTests/confs/mq/definitions.json @@ -220,4 +220,4 @@ } } ] -} \ No newline at end of file +} From 998b57fd70b6d711510d89b35beff312f471ca0a Mon Sep 17 00:00:00 2001 From: Yasin Date: Tue, 26 Nov 2024 12:01:27 +0100 Subject: [PATCH 05/11] feat: add html page for export request --- .../main/resources/static/export-request.html | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 services/localega-tsd-proxy/src/main/resources/static/export-request.html diff --git a/services/localega-tsd-proxy/src/main/resources/static/export-request.html b/services/localega-tsd-proxy/src/main/resources/static/export-request.html new file mode 100644 index 00000000..880150e7 --- /dev/null +++ b/services/localega-tsd-proxy/src/main/resources/static/export-request.html @@ -0,0 +1,166 @@ + + + + + + Export Request Form + + + + + +
+

Export Request Form

+
+
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+
+ +
+
+

This field specifies the type of the ID provided above. It determines whether the export request is for a file or a dataset. Select File ID if the ID represents a specific file, or Dataset ID if it represents an entire dataset.

+
+ +
+ +
+ +

Upload a .txt or .pem file containing the public key.

+
+
+ + +
+

You can either upload a file or paste the public key directly. Selecting one will disable the other.

+
+ +
+
+ +
+
+
+ +
+
+ + + + + From 03198c193aa5448fddd1a45e61f50f10e6b69d27 Mon Sep 17 00:00:00 2001 From: Yasin Date: Tue, 26 Nov 2024 12:04:38 +0100 Subject: [PATCH 06/11] chore: refactor ExportRequestService --- .../rest/ExportRequestController.java | 26 +------ .../no/elixir/fega/ltp/dto/ExportRequest.java | 5 +- .../ltp/services/ExportRequestService.java | 75 ++++++++++++++++--- 3 files changed, 67 insertions(+), 39 deletions(-) diff --git a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/controllers/rest/ExportRequestController.java b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/controllers/rest/ExportRequestController.java index 414fc2fa..23e36277 100644 --- a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/controllers/rest/ExportRequestController.java +++ b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/controllers/rest/ExportRequestController.java @@ -2,14 +2,10 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.constraints.NotNull; -import java.util.List; import lombok.extern.slf4j.Slf4j; import no.elixir.fega.ltp.dto.ExportRequest; import no.elixir.fega.ltp.dto.GenericResponse; import no.elixir.fega.ltp.services.ExportRequestService; -import no.elixir.fega.ltp.services.TokenService; -import no.uio.ifi.clearinghouse.model.Visa; -import no.uio.ifi.clearinghouse.model.VisaType; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -22,46 +18,28 @@ @RestController public class ExportRequestController { - private final TokenService tokenService; private final ExportRequestService exportRequestService; @Autowired - public ExportRequestController( - TokenService tokenService, ExportRequestService exportRequestService) { - this.tokenService = tokenService; + public ExportRequestController(ExportRequestService exportRequestService) { this.exportRequestService = exportRequestService; } @PostMapping("/export") public ResponseEntity exportRequest( HttpServletRequest request, @RequestBody @NotNull ExportRequest body) { - String bearerAuth = request.getHeader(HttpHeaders.PROXY_AUTHORIZATION); if (bearerAuth == null || bearerAuth.isEmpty()) { log.info("Authentication attempt without Elixir AAI access token provided"); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } - String accessToken = bearerAuth.replace("Bearer ", ""); - try { - String subject = tokenService.getSubject(accessToken); - List controlledAccessGrantsVisas = - tokenService.filterByVisaType( - tokenService.fetchTheFullPassportUsingPassportScopedAccessTokenAndGetVisas( - accessToken), - VisaType.ControlledAccessGrants); - log.info( - "Elixir user {} authenticated and provided following valid GA4GH Visas: {}", - subject, - controlledAccessGrantsVisas); + exportRequestService.exportRequest(accessToken, body); } catch (Exception e) { log.info(e.getMessage(), e); return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new GenericResponse(e.getMessage())); } - - exportRequestService.exportRequest(body); - return ResponseEntity.status(HttpStatus.OK) .body(new GenericResponse("Export request completed successfully")); } diff --git a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/dto/ExportRequest.java b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/dto/ExportRequest.java index 4c95c250..8de5560f 100644 --- a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/dto/ExportRequest.java +++ b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/dto/ExportRequest.java @@ -1,5 +1,6 @@ package no.elixir.fega.ltp.dto; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.gson.JsonObject; import jakarta.validation.constraints.NotBlank; @@ -15,9 +16,7 @@ @NoArgsConstructor public class ExportRequest { - @NotBlank(message = "The field 'jwtToken' must not be blank.") - @JsonProperty - private String jwtToken; + @JsonIgnore private String jwtToken; @NotBlank(message = "The field 'id' must not be blank.") @JsonProperty diff --git a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/services/ExportRequestService.java b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/services/ExportRequestService.java index 95fefe5a..bcd12672 100644 --- a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/services/ExportRequestService.java +++ b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/services/ExportRequestService.java @@ -1,8 +1,14 @@ package no.elixir.fega.ltp.services; +import java.util.List; +import java.util.Set; import java.util.UUID; +import java.util.regex.Pattern; +import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import no.elixir.fega.ltp.dto.ExportRequest; +import no.uio.ifi.clearinghouse.model.Visa; +import no.uio.ifi.clearinghouse.model.VisaType; import org.apache.http.entity.ContentType; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; @@ -13,6 +19,7 @@ @Service public class ExportRequestService { + private final TokenService tokenService; private final RabbitTemplate tsdRabbitTemplate; @Value("${mq.tsd.exchange}") @@ -22,21 +29,65 @@ public class ExportRequestService { private String routingKey; @Autowired - public ExportRequestService(RabbitTemplate tsdRabbitTemplate) { + public ExportRequestService(TokenService tokenService, RabbitTemplate tsdRabbitTemplate) { + this.tokenService = tokenService; this.tsdRabbitTemplate = tsdRabbitTemplate; } - public void exportRequest(ExportRequest exportRequest) { + public void exportRequest(String accessToken, ExportRequest exportRequest) throws Exception { + + String subject = tokenService.getSubject(accessToken); + List controlledAccessGrantsVisas = + tokenService.filterByVisaType( + tokenService.fetchTheFullPassportUsingPassportScopedAccessTokenAndGetVisas(accessToken), + VisaType.ControlledAccessGrants); log.info( - "Export request: {} | Exchange: {} | Routing-key: {}", exportRequest, exchange, routingKey); - tsdRabbitTemplate.convertAndSend( - exchange, - routingKey, - exportRequest.toJson(), - m -> { - m.getMessageProperties().setContentType(ContentType.APPLICATION_JSON.getMimeType()); - m.getMessageProperties().setCorrelationId(UUID.randomUUID().toString()); - return m; - }); + "Elixir user {} authenticated and provided following valid GA4GH Visas: {}", + subject, + controlledAccessGrantsVisas); + + Set collect = + controlledAccessGrantsVisas.stream() + .filter( + visa -> { + String escapedId = Pattern.quote(exportRequest.getId()); + return visa.getValue().matches(".*" + escapedId + ".*"); + }) + .collect(Collectors.toSet()); + + if (collect.isEmpty()) { + log.info( + "No visas found for user {}. Requested to export {} {}", + subject, + exportRequest.getId(), + exportRequest.getType()); + throw new Exception("No visas found"); + } + + collect.stream() + .findFirst() + .ifPresent( + (visa -> { + log.info( + "Found {} visa(s). Using the first visa to make the request.", collect.size()); + + exportRequest.setJwtToken(null); // FIXME required raw token string here + + tsdRabbitTemplate.convertAndSend( + exchange, + routingKey, + exportRequest.toJson(), + m -> { + m.getMessageProperties() + .setContentType(ContentType.APPLICATION_JSON.getMimeType()); + m.getMessageProperties().setCorrelationId(UUID.randomUUID().toString()); + return m; + }); + log.info( + "Export request: {} | Exchange: {} | Routing-key: {}", + exportRequest, + exchange, + routingKey); + })); } } From 113e83d98a87af20c1960d04f34cd8f3c3e36705 Mon Sep 17 00:00:00 2001 From: Yasin Date: Tue, 26 Nov 2024 12:08:03 +0100 Subject: [PATCH 07/11] chore: add new eof --- e2eTests/confs/mq/definitions.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/e2eTests/confs/mq/definitions.json b/e2eTests/confs/mq/definitions.json index 10860266..28c385d1 100755 --- a/e2eTests/confs/mq/definitions.json +++ b/e2eTests/confs/mq/definitions.json @@ -221,3 +221,5 @@ } ] } + + From ae475a8e92a3919ae903b4259db7922de13de477 Mon Sep 17 00:00:00 2001 From: Yasin Date: Tue, 26 Nov 2024 12:09:34 +0100 Subject: [PATCH 08/11] chore: add only one eof --- e2eTests/confs/mq/definitions.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/e2eTests/confs/mq/definitions.json b/e2eTests/confs/mq/definitions.json index 28c385d1..10860266 100755 --- a/e2eTests/confs/mq/definitions.json +++ b/e2eTests/confs/mq/definitions.json @@ -221,5 +221,3 @@ } ] } - - From c513e6cea55130df7f23e33bc7e2d530c7e80a65 Mon Sep 17 00:00:00 2001 From: Yasin Date: Tue, 26 Nov 2024 16:54:17 +0100 Subject: [PATCH 09/11] feat: remove rmq auto config and add generic exception class --- .../rest/ExportRequestController.java | 5 +++-- .../fega/ltp/exceptions/GenericException.java | 19 +++++++++++++++++++ .../ltp/services/ExportRequestService.java | 9 ++++++--- .../src/main/resources/application.yaml | 3 +++ 4 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/exceptions/GenericException.java diff --git a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/controllers/rest/ExportRequestController.java b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/controllers/rest/ExportRequestController.java index 23e36277..67db3e3e 100644 --- a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/controllers/rest/ExportRequestController.java +++ b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/controllers/rest/ExportRequestController.java @@ -5,6 +5,7 @@ import lombok.extern.slf4j.Slf4j; import no.elixir.fega.ltp.dto.ExportRequest; import no.elixir.fega.ltp.dto.GenericResponse; +import no.elixir.fega.ltp.exceptions.GenericException; import no.elixir.fega.ltp.services.ExportRequestService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; @@ -36,9 +37,9 @@ public ResponseEntity exportRequest( String accessToken = bearerAuth.replace("Bearer ", ""); try { exportRequestService.exportRequest(accessToken, body); - } catch (Exception e) { + } catch (GenericException e) { log.info(e.getMessage(), e); - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new GenericResponse(e.getMessage())); + return ResponseEntity.status(e.getHttpStatus()).body(new GenericResponse(e.getMessage())); } return ResponseEntity.status(HttpStatus.OK) .body(new GenericResponse("Export request completed successfully")); diff --git a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/exceptions/GenericException.java b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/exceptions/GenericException.java new file mode 100644 index 00000000..44a9a060 --- /dev/null +++ b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/exceptions/GenericException.java @@ -0,0 +1,19 @@ +package no.elixir.fega.ltp.exceptions; + +import java.io.Serial; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.springframework.http.HttpStatus; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class GenericException extends RuntimeException { + + @Serial private static final long serialVersionUID = 1L; + private HttpStatus httpStatus; + private String message; +} diff --git a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/services/ExportRequestService.java b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/services/ExportRequestService.java index bcd12672..419b0232 100644 --- a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/services/ExportRequestService.java +++ b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/services/ExportRequestService.java @@ -7,12 +7,14 @@ import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import no.elixir.fega.ltp.dto.ExportRequest; +import no.elixir.fega.ltp.exceptions.GenericException; import no.uio.ifi.clearinghouse.model.Visa; import no.uio.ifi.clearinghouse.model.VisaType; import org.apache.http.entity.ContentType; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; @Slf4j @@ -34,7 +36,8 @@ public ExportRequestService(TokenService tokenService, RabbitTemplate tsdRabbitT this.tsdRabbitTemplate = tsdRabbitTemplate; } - public void exportRequest(String accessToken, ExportRequest exportRequest) throws Exception { + public void exportRequest(String accessToken, ExportRequest exportRequest) + throws GenericException { String subject = tokenService.getSubject(accessToken); List controlledAccessGrantsVisas = @@ -61,7 +64,7 @@ public void exportRequest(String accessToken, ExportRequest exportRequest) throw subject, exportRequest.getId(), exportRequest.getType()); - throw new Exception("No visas found"); + throw new GenericException(HttpStatus.BAD_REQUEST, "No visas found"); } collect.stream() @@ -71,7 +74,7 @@ public void exportRequest(String accessToken, ExportRequest exportRequest) throw log.info( "Found {} visa(s). Using the first visa to make the request.", collect.size()); - exportRequest.setJwtToken(null); // FIXME required raw token string here + exportRequest.setJwtToken(visa.getRawToken()); tsdRabbitTemplate.convertAndSend( exchange, diff --git a/services/localega-tsd-proxy/src/main/resources/application.yaml b/services/localega-tsd-proxy/src/main/resources/application.yaml index 575cf219..5606845d 100644 --- a/services/localega-tsd-proxy/src/main/resources/application.yaml +++ b/services/localega-tsd-proxy/src/main/resources/application.yaml @@ -6,6 +6,9 @@ server.ssl: spring: + autoconfigure: + exclude: org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration + main: allow-circular-references: true From 4a685fba21f33db8e4a47a1e076df0a3be1c6fdb Mon Sep 17 00:00:00 2001 From: Yasin Date: Wed, 27 Nov 2024 16:38:19 +0100 Subject: [PATCH 10/11] feat: add basic auth for export request --- .../fega/ltp/config/SecurityConfig.java | 62 +++++++++++++++++++ .../rest/ExportRequestController.java | 44 ++++++------- .../no/elixir/fega/ltp/dto/ExportRequest.java | 8 ++- .../ltp/services/ExportRequestService.java | 10 +-- .../fega/ltp/services/TokenService.java | 30 ++++++++- .../src/main/resources/application.yaml | 8 ++- .../main/resources/static/export-request.html | 29 ++++++--- 7 files changed, 150 insertions(+), 41 deletions(-) create mode 100644 services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/config/SecurityConfig.java diff --git a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/config/SecurityConfig.java b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/config/SecurityConfig.java new file mode 100644 index 00000000..d92d37b2 --- /dev/null +++ b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/config/SecurityConfig.java @@ -0,0 +1,62 @@ +package no.elixir.fega.ltp.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.authentication.configurers.provisioning.InMemoryUserDetailsManagerConfigurer; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +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.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableMethodSecurity +public class SecurityConfig { + + @Value("${spring.security.user.name}") + private String username; + + @Value("${spring.security.user.password}") + private String password; + + @Value("${spring.security.user.roles}") + private String role; + + // In-memory authentication with a user named "admin" and ROLE_ADMIN + @Bean + public UserDetailsService userDetailsService() { + return new InMemoryUserDetailsManager( + User.withUsername(this.username) + .password(passwordEncoder().encode(this.password)) // encode password + .roles(this.role) // Set role from configuration + .build() + ); + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(auth -> auth + .requestMatchers("/export/**").hasRole("ADMIN") // Protect /export + .anyRequest().permitAll() // Allow all other endpoints + ) + .httpBasic(Customizer.withDefaults()) // Enable basic HTTP authentication + .csrf(AbstractHttpConfigurer::disable) // Disable CSRF for stateless API + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); // Encode passwords + } + +} diff --git a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/controllers/rest/ExportRequestController.java b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/controllers/rest/ExportRequestController.java index 67db3e3e..055618b6 100644 --- a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/controllers/rest/ExportRequestController.java +++ b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/controllers/rest/ExportRequestController.java @@ -1,6 +1,5 @@ package no.elixir.fega.ltp.controllers.rest; -import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.constraints.NotNull; import lombok.extern.slf4j.Slf4j; import no.elixir.fega.ltp.dto.ExportRequest; @@ -8,9 +7,9 @@ import no.elixir.fega.ltp.exceptions.GenericException; import no.elixir.fega.ltp.services.ExportRequestService; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @@ -19,29 +18,26 @@ @RestController public class ExportRequestController { - private final ExportRequestService exportRequestService; + private final ExportRequestService exportRequestService; - @Autowired - public ExportRequestController(ExportRequestService exportRequestService) { - this.exportRequestService = exportRequestService; - } - - @PostMapping("/export") - public ResponseEntity exportRequest( - HttpServletRequest request, @RequestBody @NotNull ExportRequest body) { - String bearerAuth = request.getHeader(HttpHeaders.PROXY_AUTHORIZATION); - if (bearerAuth == null || bearerAuth.isEmpty()) { - log.info("Authentication attempt without Elixir AAI access token provided"); - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + @Autowired + public ExportRequestController(ExportRequestService exportRequestService) { + this.exportRequestService = exportRequestService; } - String accessToken = bearerAuth.replace("Bearer ", ""); - try { - exportRequestService.exportRequest(accessToken, body); - } catch (GenericException e) { - log.info(e.getMessage(), e); - return ResponseEntity.status(e.getHttpStatus()).body(new GenericResponse(e.getMessage())); + + @PreAuthorize("hasAnyRole('ADMIN')") + @PostMapping("/export") + public ResponseEntity exportRequest(@RequestBody @NotNull ExportRequest body) { + try { + exportRequestService.exportRequest(body); + } catch (GenericException e) { + log.info(e.getMessage(), e); + return ResponseEntity.status(e.getHttpStatus()).body(new GenericResponse(e.getMessage())); + } catch (IllegalArgumentException e) { + log.info(e.getMessage(), e); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new GenericResponse(e.getMessage())); + } + return ResponseEntity.status(HttpStatus.OK) + .body(new GenericResponse("Export request completed successfully")); } - return ResponseEntity.status(HttpStatus.OK) - .body(new GenericResponse("Export request completed successfully")); - } } diff --git a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/dto/ExportRequest.java b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/dto/ExportRequest.java index 8de5560f..5fbf88cc 100644 --- a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/dto/ExportRequest.java +++ b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/dto/ExportRequest.java @@ -16,6 +16,12 @@ @NoArgsConstructor public class ExportRequest { + // This is the user's Passport Scoped Access Token. + @NotBlank(message = "The field 'accessToken' must not be blank.") + @JsonProperty + private String accessToken; + + // This is the visa token that DOA expects @JsonIgnore private String jwtToken; @NotBlank(message = "The field 'id' must not be blank.") @@ -29,7 +35,7 @@ public class ExportRequest { @NotNull(message = "The field 'type' must not be null. Should be either 'fileId' or 'datasetId'.") @JsonProperty private ExportType type = ExportType.DATASET_ID; - public String toJson() { + public String getInfoRequiredForDOAExportRequestAsJson() { JsonObject obj = new JsonObject(); obj.addProperty("jwtToken", jwtToken); obj.addProperty(type.value, id); diff --git a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/services/ExportRequestService.java b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/services/ExportRequestService.java index 419b0232..8f9e1c69 100644 --- a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/services/ExportRequestService.java +++ b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/services/ExportRequestService.java @@ -36,13 +36,13 @@ public ExportRequestService(TokenService tokenService, RabbitTemplate tsdRabbitT this.tsdRabbitTemplate = tsdRabbitTemplate; } - public void exportRequest(String accessToken, ExportRequest exportRequest) - throws GenericException { + public void exportRequest(ExportRequest exportRequest) + throws GenericException, IllegalArgumentException { - String subject = tokenService.getSubject(accessToken); + String subject = tokenService.getSubject(exportRequest.getAccessToken()); List controlledAccessGrantsVisas = tokenService.filterByVisaType( - tokenService.fetchTheFullPassportUsingPassportScopedAccessTokenAndGetVisas(accessToken), + tokenService.fetchTheFullPassportUsingPassportScopedAccessTokenAndGetVisas(exportRequest.getAccessToken()), VisaType.ControlledAccessGrants); log.info( "Elixir user {} authenticated and provided following valid GA4GH Visas: {}", @@ -79,7 +79,7 @@ public void exportRequest(String accessToken, ExportRequest exportRequest) tsdRabbitTemplate.convertAndSend( exchange, routingKey, - exportRequest.toJson(), + exportRequest.getInfoRequiredForDOAExportRequestAsJson(), m -> { m.getMessageProperties() .setContentType(ContentType.APPLICATION_JSON.getMimeType()); diff --git a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/services/TokenService.java b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/services/TokenService.java index 8624b7bf..1f6d7a88 100644 --- a/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/services/TokenService.java +++ b/services/localega-tsd-proxy/src/main/java/no/elixir/fega/ltp/services/TokenService.java @@ -7,6 +7,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.*; +import java.util.regex.Pattern; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import no.uio.ifi.clearinghouse.Clearinghouse; @@ -19,6 +20,8 @@ @Service public class TokenService { + private static final Pattern BASE64URL_PATTERN = Pattern.compile("^[A-Za-z0-9-_]+={0,2}$"); + @Value("${ga4gh.passport.openid-configuration-url}") private String openIDConfigurationURL; @@ -92,7 +95,7 @@ public List getControlledAccessGrantsVisas(String jwtToken) { * @return the subject claim (sub) as a {@link String}. * @throws NullPointerException if the JWT does not contain a subject claim. */ - public String getSubject(String jwtToken) { + public String getSubject(String jwtToken) throws IllegalArgumentException { JsonObject claims = extractFragmentFromJWT(jwtToken, TokenService.TokenFragment.BODY); return claims.get(Claims.SUBJECT).getAsString(); } @@ -112,9 +115,13 @@ public String getSubject(String jwtToken) { * @throws IllegalArgumentException if the JWT token format is invalid or the specified fragment * is missing. */ - private JsonObject extractFragmentFromJWT(String jwtToken, TokenFragment tokenFragment) { + private JsonObject extractFragmentFromJWT(String jwtToken, TokenFragment tokenFragment) throws IllegalArgumentException { var fragments = jwtToken.split("[.]"); - byte[] decodedPayload = Base64.getUrlDecoder().decode(fragments[tokenFragment.ordinal()]); + String fragment = fragments[tokenFragment.ordinal()]; + if (!isValidBase64Url(fragment)) { + throw new IllegalArgumentException("Invalid Base64 URL string"); + } + byte[] decodedPayload = Base64.getUrlDecoder().decode(fragment); String decodedPayloadString = new String(decodedPayload); return new Gson().fromJson(decodedPayloadString, JsonObject.class); } @@ -242,6 +249,23 @@ public List filterByVisaType(List visas, VisaType visaType) { .collect(Collectors.toList()); } + /** + * Validates whether the given input string is a valid Base64 URL-encoded string. + * + *

+ * A valid Base64 URL string contains only alphanumeric characters, hyphens ('-'), + * underscores ('_'), and at most two '=' padding characters. The input is + * checked against a regular expression to ensure it conforms to these rules. + *

+ * + * @param input the string to validate + * @return {@code true} if the input is a valid Base64 URL-encoded string, {@code false} otherwise + */ + private boolean isValidBase64Url(String input) { + return BASE64URL_PATTERN.matcher(input).matches(); + } + + public enum TokenFragment { HEADER, BODY, diff --git a/services/localega-tsd-proxy/src/main/resources/application.yaml b/services/localega-tsd-proxy/src/main/resources/application.yaml index 5606845d..b8bcd004 100644 --- a/services/localega-tsd-proxy/src/main/resources/application.yaml +++ b/services/localega-tsd-proxy/src/main/resources/application.yaml @@ -24,7 +24,7 @@ spring: dialect: org.hibernate.dialect.PostgreSQLDialect web: resources: - static-locations: classpath:/static/,file:/app/static + static-locations: classpath:/static/,file:/app/static/ data: redis: @@ -58,6 +58,12 @@ spring: username: ${TSD_MQ_USERNAME:admin} password: ${TSD_MQ_PASSWORD:admin} + security: + user: + name: ${ADMIN_USER:admin} # Default user if not provided + password: ${ADMIN_PASSWORD:admin123} # Default password if not provided + roles: ${ADMIN_ROLE:ADMIN} # Default role if not provided + elixir: client: id: ${CLIENT_ID} diff --git a/services/localega-tsd-proxy/src/main/resources/static/export-request.html b/services/localega-tsd-proxy/src/main/resources/static/export-request.html index 880150e7..3249570f 100644 --- a/services/localega-tsd-proxy/src/main/resources/static/export-request.html +++ b/services/localega-tsd-proxy/src/main/resources/static/export-request.html @@ -8,6 +8,8 @@ rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css" > + +