Skip to content
This repository has been archived by the owner on May 16, 2023. It is now read-only.

Commit

Permalink
feat: cancellation (#259)
Browse files Browse the repository at this point in the history
* Add Entity and Service for Portal Cancellation

* * added CancellationController and endpoint for creating cancellations.

* * added endpoint to check cancellation status

* * added endpoint to request the download/start the cancellation process

* *added checks to prevent Creating new Tests in Backend when cancellation has started

* *implemented longterm archive job for cancellation

* *implemented automatic trigger download 7 days before final deletion

* *implemented Data-Export/CSV Job

* *implemented endpoint to get Download Link

* Fix Locklimit property

* * locklimit for test properties fixed
* response code for /download swagger fixed

* * logging

* * switched from group representation id to name for partnerId

* * fixed tests for new group representation name

* * implemented Final Delete all data associated to Partner

* * changed from pocId to tenantId when encrypting

* * moved magic numbers to config

* * fixed some more getId for partnerid

* * fixed tests

* * added updateDownloadLinkRequested set after requested link

* * set updateFinalDeletion after final delete

* * set updateFinalDeletion after final delete

* Add Archive-Ignore-Check to Cancellation Archive Job

* Add Support for Legacy-Quick-Test-Partners

* Remove old MTR from CI-Master (#255) (#256)

* Remove old MTR from CI-Master

* Update ci-master.yml

* Add CancellationDate and refactor FinalDeletion Date to be calculated

* Merge Master Branch

* Add Error Logging to CSV Upload Job
Add User Logging to DownloadRequested Action for Cancellation

* Fix Cancellation Check methods

* Fix Get Quicktests Endpoint Cancellation Check

* Refactor to ZonedDateTime
Refactor CancellationJobs to work with batches

* Checkstyle

* Add own config property for complete pending tests for cancellations
Add Vault Mapping for several config properties

Co-authored-by: Felix Dittrich <[email protected]>
Co-authored-by: Morphyum <[email protected]>
Co-authored-by: Felix Dittrich <[email protected]>
  • Loading branch information
4 people authored Sep 29, 2022
1 parent 521478a commit d789809
Show file tree
Hide file tree
Showing 31 changed files with 2,085 additions and 322 deletions.
14 changes: 12 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>app.coronawarn</groupId>
Expand Down Expand Up @@ -84,7 +85,7 @@
<artifactId>keycloak-admin-client</artifactId>
</dependency>

<dependency>
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-mockmvc</artifactId>
<scope>test</scope>
Expand Down Expand Up @@ -120,6 +121,15 @@
<artifactId>gson</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.amazonaws/aws-java-sdk-s3 -->
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
</dependency>

<!-- Digital Covid Certificate -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import app.coronawarn.quicktest.archive.domain.Archive;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.RollbackException;
import javax.persistence.TypedQuery;
import lombok.RequiredArgsConstructor;
Expand All @@ -38,7 +39,7 @@ public class ArchiveRepository {

/**
* Saves the entry.
*
*
* @param archive {@link Archive}
* @return {@link Archive}
*/
Expand All @@ -59,7 +60,7 @@ public Archive save(Archive archive) {

/**
* Returns all existing entries.
*
*
* @return {@link List} of {@link Archive}
*/
public List<Archive> findAll() {
Expand Down Expand Up @@ -88,13 +89,24 @@ public List<Archive> findAllByPocId(final String pocId) {
* @return {@link List} of {@link Archive}
*/
public List<Archive> findAllByTenantId(final String tenantId) {
this.em.getTransaction().begin();
TypedQuery<Archive> query = this.em.createQuery("SELECT a FROM Archive a WHERE a.tenantId = ?1", Archive.class);
em.getTransaction().begin();
TypedQuery<Archive> query = em.createQuery("SELECT a FROM Archive a WHERE a.tenantId = ?1", Archive.class);
final List<Archive> result = query.setParameter(1, tenantId).getResultList();
this.em.getTransaction().commit();
em.getTransaction().commit();
return result;
}

/**
* Delete all entries by tenantId.
*/
public void deleteAllByTenantId(final String tenantId) {
em.getTransaction().begin();
Query query = em.createQuery("DELETE FROM Archive a WHERE a.tenantId = ?1");
query.setParameter(1, tenantId);
int rows = query.executeUpdate();
em.getTransaction().commit();
}

/**
* Returns all matching hashed guids of existing entities.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
public class ArchiveProperties {
private ArchiveJks jks = new ArchiveJks();
private Job moveToArchiveJob = new Job();
private Job cancellationArchiveJob = new Job();
private Job csvUploadJob = new Job();
private Hash hash = new Hash();
private Crypt crypt = new Crypt();
private VaultTransit vaultTransit = new VaultTransit();
Expand Down
34 changes: 34 additions & 0 deletions src/main/java/app/coronawarn/quicktest/config/CsvUploadConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package app.coronawarn.quicktest.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties("s3")
@Data
public class CsvUploadConfig {
private String accessKey;
private String secretKey;
private String bucketName;
private int expiration;

private Region region;

private ProxyConfig proxy;



@Data
public static class Region {
private String name = "";
private String endpoint;
}

@Data
public static class ProxyConfig {
private Boolean enabled = Boolean.FALSE;
private String host;
private Integer port;
}
}
11 changes: 11 additions & 0 deletions src/main/java/app/coronawarn/quicktest/config/QuickTestConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ public class QuickTestConfig {
private String pcrEnabledKey;

private FrontendContextConfig frontendContextConfig = new FrontendContextConfig();
private CancellationConfig cancellation = new CancellationConfig();

@Getter
@Setter
public static class CancellationConfig {

private int finalDeletionDays = 28;
private int completePendingTestsHours = 24;
private int readyToArchiveHours = 48;

}

@Getter
@Setter
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package app.coronawarn.quicktest.config;

import com.amazonaws.ClientConfiguration;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@RequiredArgsConstructor
@Slf4j
public class S3StorageClientConfig {

private final CsvUploadConfig s3Config;

/**
* Creates a Bean for accessing S3 storage depending on application configuration.
*
* @return Preconfigured AmazonS3 instance.
*/
@Bean
public AmazonS3 getStorage() {
ClientConfiguration clientConfig = new ClientConfiguration();
clientConfig.setSignerOverride("AWSS3V4SignerType");

if (s3Config.getProxy().getEnabled()) {
log.info("Setting proxy for S3 connection.");
clientConfig.setProxyHost(s3Config.getProxy().getHost());
clientConfig.setProxyPort(s3Config.getProxy().getPort());
}

AWSCredentials credentials = new BasicAWSCredentials(s3Config.getAccessKey(), s3Config.getSecretKey());

return AmazonS3ClientBuilder.standard()
.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(
s3Config.getRegion().getEndpoint(), s3Config.getRegion().getName()))
.withPathStyleAccessEnabled(true)
.withClientConfiguration(clientConfig)
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
public static final String ROLE_ADMIN = "ROLE_c19_quick_test_admin";
public static final String ROLE_TENANT_COUNTER = "ROLE_c19_quick_tenant_test_counter";
public static final String ROLE_POC_NAT_ADMIN = "ROLE_c19_quick_test_poc_nat_admin";
public static final String ROLE_TERMINATOR = "ROLE_c19_quick_test_terminator";

private static final String API_ROUTE = "/api/**";
private static final String CONFIG_ROUTE = "/api/config/*";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package app.coronawarn.quicktest.controller;

import static app.coronawarn.quicktest.config.SecurityConfig.ROLE_TERMINATOR;

import app.coronawarn.quicktest.config.CsvUploadConfig;
import app.coronawarn.quicktest.domain.Cancellation;
import app.coronawarn.quicktest.model.cancellation.CancellationRequest;
import app.coronawarn.quicktest.service.CancellationService;
import app.coronawarn.quicktest.utils.Utilities;
import com.amazonaws.HttpMethod;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import java.net.URL;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;


@Slf4j
@RestController
@RequestMapping(value = "/api/cancellation")
@RequiredArgsConstructor
public class CancellationController {

private final CsvUploadConfig s3Config;

private final AmazonS3 s3Client;

private final CancellationService cancellationService;

private final Utilities utils;

/**
* Endpoint for creating new cancellations.
*
* @return List of successfully reported PartnerIds
*/
@Operation(
summary = "Creates a cancellation entry for each given PartnerId",
description = "Creates a cancellation entry for each given PartnerId and returns a list of them."
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successful")
})
@PostMapping(value = "", produces = MediaType.APPLICATION_JSON_VALUE)
@Secured({ROLE_TERMINATOR})
public ResponseEntity<List<Cancellation>> createCancellations(@RequestBody CancellationRequest request) {
List<Cancellation> cancellations = new ArrayList<>();

for (String partnerId : request.getPartnerIds()) {
cancellations.add(cancellationService.createCancellation(
partnerId, request.getCancellationDate().atZone(ZoneId.of("UTC"))));
}

return ResponseEntity.ok(cancellations);
}

/**
* Endpoint for receiving information about cancellations.
*
* @return Cancellation information for users PartnerId
*/
@Operation(
summary = "Returns information about a cancellation",
description = "Returns Information about a cancellation for the partnerId associated to the requesting user."
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successful"),
@ApiResponse(responseCode = "404", description = "No cancellation found for given PartnerId.")
})
@GetMapping(value = "", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Cancellation> getCancellation() {
Optional<Cancellation> cancellation = cancellationService.getByPartnerId(utils.getTenantIdFromToken());

if (cancellation.isPresent()) {
return ResponseEntity.ok(cancellation.get());
} else {
throw new ResponseStatusException(
HttpStatus.NOT_FOUND, "No cancellation found for given PartnerId.");
}
}

/**
* Endpoint for receiving download link of tenant.
*
* @return Download link of csv for tenant
*/
@Operation(
summary = "Returns Download link of csv for tenant",
description = "Returns Download link of csv with all quicktests for tenant."
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successful"),
@ApiResponse(responseCode = "400", description = "Download Link for given PartnerId not yet available, "
+ "cancellation might not have been processed yet."),
@ApiResponse(responseCode = "404", description = "No cancellation found for given PartnerId.")
})
@GetMapping(value = "/download", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<URL> getCsvLink() {
Optional<Cancellation> cancellation = cancellationService.getByPartnerId(utils.getTenantIdFromToken());

if (cancellation.isPresent() && ZonedDateTime.now().isAfter(cancellation.get().getCancellationDate())) {

if (cancellation.get().getCsvCreated() != null && cancellation.get().getBucketObjectId() != null) {
long expTimeMillis = Instant.now().toEpochMilli();
expTimeMillis += s3Config.getExpiration();
Date expiration = new Date();
expiration.setTime(expTimeMillis);
GeneratePresignedUrlRequest generatePresignedUrlRequest =
new GeneratePresignedUrlRequest(s3Config.getBucketName(), cancellation.get().getBucketObjectId())
.withMethod(HttpMethod.GET)
.withExpiration(expiration);
cancellationService.updateDownloadLinkRequested(
cancellation.get(), ZonedDateTime.now(), utils.getUserNameFromToken());
return ResponseEntity.ok(s3Client.generatePresignedUrl(generatePresignedUrlRequest));
} else {
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST,
"Download Link for given PartnerId not yet available, "
+ "cancellation might not have been processed yet.");
}
} else {
throw new ResponseStatusException(
HttpStatus.NOT_FOUND, "No cancellation found for given PartnerId.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import app.coronawarn.quicktest.config.QuickTestConfig;
import app.coronawarn.quicktest.model.keycloak.KeyCloakConfigFile;
import app.coronawarn.quicktest.model.quicktest.QuickTestContextFile;
import app.coronawarn.quicktest.service.MapEntryService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.keycloak.adapters.springboot.KeycloakSpringBootProperties;
Expand Down Expand Up @@ -60,6 +59,8 @@ public ResponseEntity<QuickTestContextFile> getQuickTestContextFile() {
return ResponseEntity.ok(
new QuickTestContextFile(
quickTestConfig.getFrontendContextConfig().getRulesServerUrl(),
quickTestConfig.getFrontendContextConfig().getEnvironmentName()));
quickTestConfig.getFrontendContextConfig().getEnvironmentName(),
quickTestConfig.getCancellation().getCompletePendingTestsHours()
));
}
}
Loading

0 comments on commit d789809

Please sign in to comment.