Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: export request to sda doa #304

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
19 changes: 18 additions & 1 deletion e2eTests/confs/mq/definitions.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@
"auto_delete": false,
"arguments": {
}
},
{
"name": "exportRequests",
"vhost": "<<VIRTUAL_HOST>>",
"durable": true,
"auto_delete": false,
"arguments": {
}
}
],
"exchanges": [
Expand Down Expand Up @@ -201,6 +209,15 @@
"routing_key": "files",
"arguments": {
}
},
{
"source": "sda",
"vhost": "<<VIRTUAL_HOST>>",
"destination": "exportRequests",
"destination_type": "queue",
"routing_key": "exportRequests",
"arguments": {
}
}
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,25 +32,25 @@ public class PublishMQAspect {

private final Gson gson;

private final RabbitTemplate rabbitTemplate;
private final RabbitTemplate cegaRabbitTemplate;

@Value("${tsd.project}")
private String tsdProjectId;

@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;
}

/**
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package no.elixir.fega.ltp.controllers.rest;

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.exceptions.GenericException;
import no.elixir.fega.ltp.services.ExportRequestService;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
public class ExportRequestController {

private final ExportRequestService exportRequestService;

@Autowired
public ExportRequestController(ExportRequestService exportRequestService) {
this.exportRequestService = exportRequestService;
}

@PreAuthorize("hasAnyRole('ADMIN')")
@PostMapping("/export")
public ResponseEntity<GenericResponse> 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"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
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;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Data
@ToString
@AllArgsConstructor
@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.")
@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 getInfoRequiredForDOAExportRequestAsJson() {
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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package no.elixir.fega.ltp.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class GenericResponse {

private String message;
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
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.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
@Service
public class ExportRequestService {

private final TokenService tokenService;
private final RabbitTemplate tsdRabbitTemplate;

@Value("${mq.tsd.exchange}")
private String exchange;

@Value("${mq.tsd.routing-key}")
private String routingKey;

@Autowired
public ExportRequestService(TokenService tokenService, RabbitTemplate tsdRabbitTemplate) {
this.tokenService = tokenService;
this.tsdRabbitTemplate = tsdRabbitTemplate;
}

public void exportRequest(ExportRequest exportRequest)
throws GenericException, IllegalArgumentException {

String subject = tokenService.getSubject(exportRequest.getAccessToken());
List<Visa> controlledAccessGrantsVisas =
tokenService.filterByVisaType(
tokenService.fetchTheFullPassportUsingPassportScopedAccessTokenAndGetVisas(
exportRequest.getAccessToken()),
VisaType.ControlledAccessGrants);
log.info(
"Elixir user {} authenticated and provided following valid GA4GH Visas: {}",
subject,
controlledAccessGrantsVisas);

Set<Visa> 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 GenericException(HttpStatus.BAD_REQUEST, "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(visa.getRawToken());

tsdRabbitTemplate.convertAndSend(
exchange,
routingKey,
exportRequest.getInfoRequiredForDOAExportRequestAsJson(),
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);
}));
}
}
Loading
Loading