Skip to content

Commit

Permalink
Merge pull request #65 from kbss-cvut/publish-fix
Browse files Browse the repository at this point in the history
Permit all
  • Loading branch information
blcham authored Sep 17, 2024
2 parents 124bc3e + 80ea14b commit 01887c8
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 59 deletions.
22 changes: 22 additions & 0 deletions src/main/java/cz/cvut/kbss/study/dto/RecordImportResult.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package cz.cvut.kbss.study.dto;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
* Represents the result of importing records to this instance.
Expand All @@ -18,6 +20,8 @@ public class RecordImportResult {
*/
private int importedCount;

private Set<String> importedRecords;

/**
* Errors that occurred during import.
*/
Expand Down Expand Up @@ -46,6 +50,24 @@ public void setImportedCount(int importedCount) {
this.importedCount = importedCount;
}


public Set<String> getImportedRecords() {
return importedRecords;
}

public void setImportedRecords(Set<String> importedRecords) {
this.importedRecords = importedRecords;
}

public void addImportedRecord(String recordIRI) {
if(recordIRI == null)
return;
if(importedRecords == null)
importedRecords = new HashSet<>();
importedRecords.add(recordIRI);
importedCount = importedRecords.size();
}

public void incrementImportedCount() {
this.importedCount++;
}
Expand Down
65 changes: 8 additions & 57 deletions src/main/java/cz/cvut/kbss/study/rest/PatientRecordController.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package cz.cvut.kbss.study.rest;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import cz.cvut.kbss.study.dto.PatientRecordDto;
Expand All @@ -16,17 +15,13 @@
import cz.cvut.kbss.study.rest.util.RecordFilterMapper;
import cz.cvut.kbss.study.rest.util.RestUtils;
import cz.cvut.kbss.study.security.SecurityConstants;
import cz.cvut.kbss.study.service.ConfigReader;
import cz.cvut.kbss.study.service.ExcelRecordConverter;
import cz.cvut.kbss.study.service.PatientRecordService;
import cz.cvut.kbss.study.service.UserService;
import cz.cvut.kbss.study.service.*;
import cz.cvut.kbss.study.util.ConfigParam;
import cz.cvut.kbss.study.util.Constants;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.InputStreamResource;
import org.springframework.data.domain.Page;
import org.springframework.http.*;
Expand Down Expand Up @@ -57,20 +52,22 @@ public class PatientRecordController extends BaseController {
private final ExcelRecordConverter excelRecordConverter;
private final RestTemplate restTemplate;
private final ConfigReader configReader;
private final PublishRecordsService publishRecordsService;
private ObjectMapper objectMapper;
private final UserService userService;

public PatientRecordController(PatientRecordService recordService, ApplicationEventPublisher eventPublisher,
ExcelRecordConverter excelRecordConverter, RestTemplate restTemplate,
ConfigReader configReader, ObjectMapper objectMapper,
UserService userService) {
UserService userService, PublishRecordsService publishRecordsService) {
this.recordService = recordService;
this.eventPublisher = eventPublisher;
this.excelRecordConverter = excelRecordConverter;
this.restTemplate = restTemplate;
this.configReader = configReader;
this.objectMapper = objectMapper;
this.userService = userService;
this.publishRecordsService = publishRecordsService;
}

@PreAuthorize("hasRole('" + SecurityConstants.ROLE_ADMIN + "') or #institutionKey==null or @securityUtils.isMemberOfInstitution(#institutionKey)")
Expand Down Expand Up @@ -200,58 +197,12 @@ public RecordImportResult publishRecords(
@RequestParam(required = false) MultiValueMap<String, String> params,
HttpServletRequest request) {

String onPublishRecordsServiceUrl = configReader.getConfig(ConfigParam.ON_PUBLISH_RECORDS_SERVICE_URL);
if(onPublishRecordsServiceUrl == null || onPublishRecordsServiceUrl.isBlank()) {
LOG.info("No publish service url provided, noop.");
RecordImportResult result = new RecordImportResult(0);
result.addError("Cannot publish completed records. Publish server not configured.");
return result;
}

// export
final Page<PatientRecord> result = recordService.findAllFull(RecordFilterMapper.constructRecordFilter(params),
RestUtils.resolvePaging(params));
List<PatientRecord> records = result.getContent();

// Convert the records to JSON
String recordsJson;
try {
recordsJson = objectMapper.writeValueAsString(records);
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to convert records to JSON", e);
}

// Create a MultiValueMap to hold the file part
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("file", new ByteArrayResource(recordsJson.getBytes()) {
@Override
public String getFilename() {
return "records.json";
}
});

// Create HttpEntity
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (authHeader != null && !authHeader.isBlank()) {
headers.set(HttpHeaders.AUTHORIZATION, authHeader);
} else {
throw new RuntimeException("Authorization header missing in request");
}
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);

// Call the import endpoint
LOG.debug("Publishing records.");
ResponseEntity<RecordImportResult> responseEntity = restTemplate.postForEntity(
onPublishRecordsServiceUrl, requestEntity, RecordImportResult.class);

// TODO make records published

LOG.debug("Publish server response: ", responseEntity.getBody());
return responseEntity.getBody();
RecordFilterParams filterParameters = RecordFilterMapper.constructRecordFilter(params);
RecordImportResult importResult = publishRecordsService.publishRecords(filterParameters, RestUtils.resolvePaging(params));
return importResult;
}

@PreAuthorize("permitAll()")
@PostMapping(value = "/import/json", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public RecordImportResult importRecordsJson(@RequestPart("file") MultipartFile file,
@RequestParam(name = "phase", required = false) String phase) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,12 @@ public interface PatientRecordService extends BaseService<PatientRecord> {
Set<RecordPhase> findUsedRecordPhases();


/**
* For all records identified but recordsUris sets record phase to targetPhase.
* @param recordUris
* @param targetPhase
*/
void setPhase(Set<String> recordUris, RecordPhase targetPhase);


}
105 changes: 105 additions & 0 deletions src/main/java/cz/cvut/kbss/study/service/PublishRecordsService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package cz.cvut.kbss.study.service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import cz.cvut.kbss.study.config.WebAppConfig;
import cz.cvut.kbss.study.dto.RecordImportResult;
import cz.cvut.kbss.study.model.PatientRecord;
import cz.cvut.kbss.study.model.RecordPhase;
import cz.cvut.kbss.study.persistence.dao.util.RecordFilterParams;
import cz.cvut.kbss.study.service.security.SecurityUtils;
import cz.cvut.kbss.study.util.ConfigParam;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import java.util.HashSet;
import java.util.List;

@Service
public class PublishRecordsService {

private static final Logger LOG = LoggerFactory.getLogger(PublishRecordsService.class);

private final PatientRecordService recordService;
private final ObjectMapper objectMapper;
private final SecurityUtils securityUtils;
private final RestTemplate restTemplate;
private final ConfigReader configReader;

public PublishRecordsService(PatientRecordService recordService, SecurityUtils securityUtils, RestTemplate restTemplate, ConfigReader configReader) {
this.recordService = recordService;
this.objectMapper = WebAppConfig.createJsonObjectMapper();
this.securityUtils = securityUtils;
this.restTemplate = restTemplate;
this.configReader = configReader;
}

public RecordImportResult publishRecords(RecordFilterParams filters, Pageable pageSpec){
String onPublishRecordsServiceUrl = configReader.getConfig(ConfigParam.ON_PUBLISH_RECORDS_SERVICE_URL);
if(onPublishRecordsServiceUrl == null || onPublishRecordsServiceUrl.isBlank()) {
LOG.warn("No publish service url configured, noop.");
RecordImportResult result = new RecordImportResult(0);
result.addError("Cannot publish completed records. Publish server not configured.");
return result;
}

filters.setPhaseIds(new HashSet<>());
filters.getPhaseIds().add(RecordPhase.completed.getIri());

final Page<PatientRecord> result = recordService.findAllFull(filters, pageSpec);
List<PatientRecord> records = result.getContent();

ResponseEntity<RecordImportResult> responseEntity = executePublishRequest(onPublishRecordsServiceUrl, records);

LOG.debug("Publish server response: {}", responseEntity.getBody());
RecordImportResult importResult = responseEntity.getBody();
if(importResult != null && importResult.getImportedRecords() != null)
recordService.setPhase(importResult.getImportedRecords(), RecordPhase.published);
return importResult;
}

protected ResponseEntity<RecordImportResult> executePublishRequest(String onPublishRecordsServiceUrl, List<PatientRecord> records){
String recordsJson;
try {
recordsJson = objectMapper.writeValueAsString(records);
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to convert records to JSON", e);
}

// Create a MultiValueMap to hold the file part
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("file", new ByteArrayResource(recordsJson.getBytes()) {
@Override
public String getFilename() {
return "records.json";
}
});

// Create HttpEntity
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
String authHeader = securityUtils.getPublishToken();
if (authHeader != null && !authHeader.isBlank()) {
headers.setBearerAuth(authHeader);
} else {
throw new SecurityException("Could not retrieve publish token.");
}
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);

// Call the import endpoint
LOG.debug("Publishing records.");
return restTemplate.postForEntity(
onPublishRecordsServiceUrl, requestEntity, RecordImportResult.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.net.URI;
import java.util.*;

@Service
Expand Down Expand Up @@ -96,7 +97,7 @@ private RecordImportResult importRecordsImpl(List<PatientRecord> records, Option
result.addError("Record " + Utils.uriToString(r.getUri()) + " already exists.");
} else {
recordDao.persist(r);
result.incrementImportedCount();
result.addImportedRecord(r.getUri().toString());
}
});
return result;
Expand All @@ -118,6 +119,16 @@ private void setImportedRecordProvenance(User currentUser, Date now, Optional<Re
}
}

@Transactional
@Override
public void setPhase(Set<String> recordUris, RecordPhase targetPhase){
for(String uri : recordUris){
PatientRecord record = find(URI.create(uri));
if (record == null)
continue;
record.setPhase(targetPhase);
}
}

@Transactional
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,28 @@
import cz.cvut.kbss.study.security.model.Role;
import cz.cvut.kbss.study.security.model.UserDetails;
import cz.cvut.kbss.study.service.ConfigReader;
import cz.cvut.kbss.study.util.ConfigParam;
import cz.cvut.kbss.study.util.oidc.OidcGrantedAuthoritiesExtractor;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.web.server.authentication.SwitchUserWebFilter;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import java.util.List;
import java.util.Map;
import java.util.Optional;

@Service
Expand All @@ -31,11 +41,13 @@ public class SecurityUtils {
private final PatientRecordDao patientRecordDao;

private final ConfigReader config;
private final RestTemplate restTemplate;

public SecurityUtils(UserDao userDao, PatientRecordDao patientRecordDao, ConfigReader config) {
public SecurityUtils(UserDao userDao, PatientRecordDao patientRecordDao, ConfigReader config, RestTemplate restTemplate) {
this.userDao = userDao;
this.patientRecordDao = patientRecordDao;
this.config = config;
this.restTemplate = restTemplate;
}

/**
Expand Down Expand Up @@ -128,4 +140,41 @@ public boolean areFromSameInstitution(String username) {
final List<User> users = userDao.findByInstitution(user.getInstitution());
return users.stream().anyMatch(o -> o.getUsername().equals(username));
}

public String getCurrentToken(){
// Retrieve token from security context
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// Check if token is Jwt
if(! (authentication.getPrincipal() instanceof Jwt))
throw new IllegalArgumentException("Cannot process request, authentication principal type \"%s\" is not supported.".formatted(authentication.getPrincipal().getClass()));

// This is only for Jwt type tokens
return ((Jwt)authentication.getPrincipal()).getTokenValue();
}

public String getPublishToken(){
// TODO - The exchanged token does not contain an IDP field, so the supplier won't know the original identity provider
String accessToken = getCurrentToken();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange");
map.add("subject_token", accessToken);
map.add("subject_token_type", "urn:ietf:params:oauth:token-type:jwt");
map.add("client_id", "record-manager-server"); // The client_id of the publishing service, i.e. the suppliers record-manager-server
map.add("client_secret", config.getConfig(ConfigParam.PUBLISH_RECORDS_SERVICE_SECRET)); // the client secret for the client_id

//TODO consider adding other parameters to the `map`
// as described in https://github.com/kbss-cvut/23ava-distribution/issues/147#issuecomment-2356420098

try {
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);
ResponseEntity<Map> response = restTemplate.postForEntity(config.getConfig(ConfigParam.EXCHANGE_TOKEN_SERVICE_URL), request, Map.class);

return response.getBody().get("access_token").toString();

} catch (Exception e) {
throw new SecurityException("Error exchanging token", e);
}
}
}
Loading

0 comments on commit 01887c8

Please sign in to comment.