Skip to content

Commit

Permalink
Add CMS migration controller (#153)
Browse files Browse the repository at this point in the history
Co-authored-by: Felix Dittrich <[email protected]>
  • Loading branch information
bergmann-dierk and f11h committed Feb 21, 2022
1 parent 925b815 commit 7c9e0cc
Show file tree
Hide file tree
Showing 11 changed files with 1,198 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,5 @@ public interface RevocationBatchRepository extends JpaRepository<RevocationBatch

List<RevocationBatchProjection> getAllByChangedGreaterThanOrderByChangedAsc(ZonedDateTime date, Pageable page);

List<RevocationBatchEntity> getAllByCountry(String country);
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,6 @@ List<ValidationRuleEntity> getByRuleIdAndValidFromIsGreaterThanEqualOrderByIdDes

Optional<ValidationRuleEntity> getByRuleIdAndVersion(String ruleId, String version);

List<ValidationRuleEntity> getAllByCountry(String country);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
package eu.europa.ec.dgc.gateway.restapi.controller;

import static eu.europa.ec.dgc.gateway.utils.CmsUtils.getSignedString;
import static eu.europa.ec.dgc.gateway.utils.CmsUtils.getSignerCertificate;

import eu.europa.ec.dgc.gateway.config.OpenApiConfig;
import eu.europa.ec.dgc.gateway.exception.DgcgResponseException;
import eu.europa.ec.dgc.gateway.restapi.dto.CmsPackageDto;
import eu.europa.ec.dgc.gateway.restapi.dto.SignedCertificateDto;
import eu.europa.ec.dgc.gateway.restapi.dto.SignedStringDto;
import eu.europa.ec.dgc.gateway.restapi.filter.CertificateAuthenticationFilter;
import eu.europa.ec.dgc.gateway.restapi.filter.CertificateAuthenticationRequired;
import eu.europa.ec.dgc.gateway.service.RevocationListService;
import eu.europa.ec.dgc.gateway.service.SignerInformationService;
import eu.europa.ec.dgc.gateway.service.ValidationRuleService;
import eu.europa.ec.dgc.gateway.utils.DgcMdc;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import java.util.List;
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.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
@RequestMapping("/cms-migration")
@Slf4j
@RequiredArgsConstructor
public class CertificateMigrationController {

public static final String SENT_VALUES_FORMAT = "{%s} country:{%s}";
public static final String X_004 = "0x004";
public static final String DEFAULT_ERROR_MESSAGE = "Possible reasons: Wrong Format,"
+ " no CMS, not the correct signing alg missing attributes, invalid signature, "
+ "certificate not signed by known CA";

private final SignerInformationService signerInformationService;

private final RevocationListService revocationListService;

private final ValidationRuleService validationRuleService;

private static final String MDC_VERIFICATION_ERROR_REASON = "verificationFailureReason";
private static final String MDC_VERIFICATION_ERROR_MESSAGE = "verificationFailureMessage";

/**
* Get CMS Packages for country.
*/
@CertificateAuthenticationRequired
@GetMapping
@Operation(
security = {
@SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_HASH),
@SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_DISTINGUISH_NAME)
},
summary = "Get all cms packages for a country identified by certificate.",
tags = {"CMS Migration"},
responses = {
@ApiResponse(
responseCode = "200",
description = "Download successful.",
content = @Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = CmsPackageDto.class)
)
)
}
)
public ResponseEntity<List<CmsPackageDto>> getCmsPackages(
@RequestAttribute(CertificateAuthenticationFilter.REQUEST_PROP_COUNTRY) String countryCode
) {

log.info("Getting cms packages for {}", countryCode);

List<CmsPackageDto> listItems = signerInformationService.getCmsPackage(countryCode);
log.info("Found {} signerInformation DSC entries", listItems.size());

List<CmsPackageDto> revocationList = revocationListService.getCmsPackage(countryCode);
log.info("Found {} revocation entries", revocationList.size());
listItems.addAll(revocationList);

List<CmsPackageDto> validationList = validationRuleService.getCmsPackage(countryCode);
log.info("Found {} validation rule entries", validationList.size());
listItems.addAll(validationList);

return ResponseEntity.ok(listItems);
}

/**
* Update a CMS Package.
*
*/
@CertificateAuthenticationRequired
@PostMapping
@Operation(
security = {
@SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_HASH),
@SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_DISTINGUISH_NAME)
},
tags = {"CMS Migration"},
summary = "Update an existing CMS Package",
description = "Endpoint to update an existing CMS pacakage.",
requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
required = true,
content = @Content(schema = @Schema(implementation = CmsPackageDto.class))
),
responses = {
@ApiResponse(
responseCode = "204",
description = "Update applied."),
@ApiResponse(
responseCode = "409",
description = "CMS Package does not exist."),
@ApiResponse(
responseCode = "400",
description = "Invalid CMS input.")
}
)
public ResponseEntity<Void> updateCmsPackage(
@RequestBody CmsPackageDto cmsPackageDto,
@RequestAttribute(CertificateAuthenticationFilter.REQUEST_PROP_COUNTRY) String countryCode,
@RequestAttribute(CertificateAuthenticationFilter.REQUEST_PROP_THUMBPRINT) String authThumbprint
) {

if (CmsPackageDto.CmsPackageTypeDto.DSC == cmsPackageDto.getType()) {
SignedCertificateDto signedCertificateDto = getSignerCertificate(cmsPackageDto.getCms());
if (!signedCertificateDto.isVerified()) {
throw new DgcgResponseException(HttpStatus.BAD_REQUEST, "0x260", "CMS signature is invalid", "",
"Submitted package needs to be signed by a valid upload certificate");
}

try {
signerInformationService.updateSignerCertificate(cmsPackageDto.getEntityId(),
signedCertificateDto.getPayloadCertificate(), signedCertificateDto.getSignerCertificate(),
signedCertificateDto.getSignature(), countryCode);
} catch (SignerInformationService.SignerCertCheckException e) {
handleSignerCertException(cmsPackageDto, countryCode, e);
}
} else {
SignedStringDto signedStringDto = getSignedString(cmsPackageDto.getCms());

if (!signedStringDto.isVerified()) {
throw new DgcgResponseException(HttpStatus.BAD_REQUEST, "0x260", "CMS signature is invalid", "",
"Submitted package needs to be signed by a valid upload certificate");
}
try {
if (CmsPackageDto.CmsPackageTypeDto.REVOCATION_LIST == cmsPackageDto.getType()) {
revocationListService.updateRevocationBatchCertificate(cmsPackageDto.getEntityId(),
signedStringDto.getPayloadString(), signedStringDto.getSignerCertificate(),
signedStringDto.getRawMessage(), countryCode);
} else if (CmsPackageDto.CmsPackageTypeDto.VALIDATION_RULE == cmsPackageDto.getType()) {
validationRuleService.updateValidationRuleCertificate(cmsPackageDto.getEntityId(),
signedStringDto.getPayloadString(), signedStringDto.getSignerCertificate(),
signedStringDto.getRawMessage(), countryCode);
}
} catch (RevocationListService.RevocationBatchServiceException e) {
handleRevocationBatchException(cmsPackageDto, countryCode, e);
} catch (ValidationRuleService.ValidationRuleCheckException e) {
handleValidationRuleExcepetion(cmsPackageDto, countryCode, e);
}
}

return ResponseEntity.noContent().build();
}

private void updateMdc(String s, String message) {
DgcMdc.put(MDC_VERIFICATION_ERROR_REASON, s);
DgcMdc.put(MDC_VERIFICATION_ERROR_MESSAGE, message);
log.error("CMS migration failed");
}

private void handleSignerCertException(CmsPackageDto cmsPackageDto, String countryCode,
SignerInformationService.SignerCertCheckException e) {
updateMdc(e.getReason().toString(), e.getMessage());
String sentValues = String.format(SENT_VALUES_FORMAT, cmsPackageDto, countryCode);
switch (e.getReason()) {
case EXIST_CHECK_FAILED:
throw new DgcgResponseException(HttpStatus.CONFLICT, "0x010",
"Certificate to be updated does not exist.",
sentValues, e.getMessage());
case UPLOAD_FAILED:
throw new DgcgResponseException(HttpStatus.INTERNAL_SERVER_ERROR,
"0x011", "Upload of new Signer Certificate failed", sentValues, e.getMessage());
default:
throw new DgcgResponseException(HttpStatus.BAD_REQUEST, X_004, DEFAULT_ERROR_MESSAGE, sentValues,
e.getMessage());
}
}

private void handleRevocationBatchException(CmsPackageDto cmsPackageDto, String countryCode,
RevocationListService.RevocationBatchServiceException e) {
updateMdc(e.getReason().toString(), e.getMessage());
String sentValues = String.format(SENT_VALUES_FORMAT, cmsPackageDto, countryCode);
switch (e.getReason()) {
case NOT_FOUND:
throw new DgcgResponseException(HttpStatus.CONFLICT, "0x020",
"RevocationBatch to be updated does not exist.",
sentValues, e.getMessage());
case INVALID_COUNTRY:
throw new DgcgResponseException(HttpStatus.BAD_REQUEST,
"0x021", "Invalid country", sentValues, e.getMessage());
case INVALID_JSON_VALUES:
throw new DgcgResponseException(HttpStatus.BAD_REQUEST,
"0x022", "Json Payload invalid", sentValues, e.getMessage());
default:
throw new DgcgResponseException(HttpStatus.BAD_REQUEST, X_004, DEFAULT_ERROR_MESSAGE, sentValues,
e.getMessage());
}
}

private void handleValidationRuleExcepetion(CmsPackageDto cmsPackageDto, String countryCode,
ValidationRuleService.ValidationRuleCheckException e) {
updateMdc(e.getReason().toString(), e.getMessage());
String sentValues = String.format(SENT_VALUES_FORMAT, cmsPackageDto, countryCode);
switch (e.getReason()) {
case NOT_FOUND:
throw new DgcgResponseException(HttpStatus.CONFLICT, "0x030",
"ValidationRule to be updated does not exist.",
sentValues, e.getMessage());
case INVALID_COUNTRY:
throw new DgcgResponseException(HttpStatus.BAD_REQUEST,
"0x031", "Invalid country", sentValues, e.getMessage());
case INVALID_JSON:
throw new DgcgResponseException(HttpStatus.BAD_REQUEST, "0x032", "Json Payload invalid", sentValues,
e.getMessage());
default:
throw new DgcgResponseException(HttpStatus.BAD_REQUEST, X_004, DEFAULT_ERROR_MESSAGE, sentValues,
e.getMessage());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package eu.europa.ec.dgc.gateway.restapi.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;

@Schema(name = "CmsPackage")
@Data
@AllArgsConstructor
public class CmsPackageDto {

@Schema(description = "CMS containing the signed String or certificate")
private String cms;

@Schema(description = "Internal ID of the package")
private Long entityId;

@Schema(description = "Type of the CMS package")
private CmsPackageTypeDto type;

public enum CmsPackageTypeDto {
DSC,
REVOCATION_LIST,
VALIDATION_RULE
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

package eu.europa.ec.dgc.gateway.service;

import static eu.europa.ec.dgc.gateway.utils.CmsUtils.getSignedString;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
Expand All @@ -29,6 +31,8 @@
import eu.europa.ec.dgc.gateway.model.RevocationBatchDownload;
import eu.europa.ec.dgc.gateway.model.RevocationBatchList;
import eu.europa.ec.dgc.gateway.repository.RevocationBatchRepository;
import eu.europa.ec.dgc.gateway.restapi.dto.CmsPackageDto;
import eu.europa.ec.dgc.gateway.restapi.dto.SignedStringDto;
import eu.europa.ec.dgc.gateway.restapi.dto.revocation.RevocationBatchDeleteRequestDto;
import eu.europa.ec.dgc.gateway.restapi.dto.revocation.RevocationBatchDto;
import eu.europa.ec.dgc.gateway.utils.DgcMdc;
Expand Down Expand Up @@ -239,12 +243,77 @@ public RevocationBatchDownload getRevocationBatch(String batchId) throws Revocat
return new RevocationBatchDownload(entity.get().getBatchId(), entity.get().getSignedBatch());
}

/**
* Get CMS packages for country.
*
* @param country The country
* @return A list of all CMS Packages.
*/
public List<CmsPackageDto> getCmsPackage(String country) {
List<RevocationBatchEntity> revocationBatchEntities = revocationBatchRepository.getAllByCountry(country);
return revocationBatchEntities.stream()
.map(it -> new CmsPackageDto(it.getSignedBatch(), it.getId(),
CmsPackageDto.CmsPackageTypeDto.REVOCATION_LIST))
.collect(Collectors.toList());
}

/**
* Update Revocation Batch with new cms in db.
*
* @param id the id of the entity.
* @param signerCertificate the certificate which was used to sign the message
* @param cms the cms containing the JSON
* @param authenticatedCountryCode the country code of the uploader country from cert authentication
* @throws RevocationBatchServiceException if validation check has failed. The exception contains a reason property
* with detailed information why the validation has failed.
*/
public RevocationBatchEntity updateRevocationBatchCertificate(
Long id,
String payloadRevocationBatch,
X509CertificateHolder signerCertificate,
String cms,
String authenticatedCountryCode
) throws RevocationBatchServiceException {

final RevocationBatchEntity revocationBatchEntity = revocationBatchRepository.findById(id).orElseThrow(
() -> new RevocationBatchServiceException(RevocationBatchServiceException.Reason.NOT_FOUND,
"RevocationBatch not present."));

contentCheckUploaderCertificate(signerCertificate, authenticatedCountryCode);
contentCheckUploaderCountry(revocationBatchEntity.getCountry(), authenticatedCountryCode);
contentCheckMigrateCms(payloadRevocationBatch, revocationBatchEntity.getSignedBatch());

revocationBatchEntity.setSignedBatch(cms);
revocationBatchEntity.setChanged(ZonedDateTime.now());

log.info("Updating cms of Revocation Batch Entity with id {}", revocationBatchEntity.getBatchId());

auditService.addAuditEvent(
authenticatedCountryCode,
signerCertificate,
authenticatedCountryCode,
"UPDATED",
String.format("Updated Revocation Batch (%s)", revocationBatchEntity.getBatchId())
);

RevocationBatchEntity updatedEntity = revocationBatchRepository.save(revocationBatchEntity);

DgcMdc.remove(MDC_PROP_UPLOAD_CERT_THUMBPRINT);

return updatedEntity;
}

private void contentCheckUploaderCountry(RevocationBatchDto parsedBatch, String countryCode)
throws RevocationBatchServiceException {
if (!parsedBatch.getCountry().equals(countryCode)) {
contentCheckUploaderCountry(parsedBatch.getCountry(), countryCode);
}

private void contentCheckUploaderCountry(String batchCountryCode, String countryCode)
throws RevocationBatchServiceException {
if (!batchCountryCode.equals(countryCode)) {
throw new RevocationBatchServiceException(
RevocationBatchServiceException.Reason.INVALID_COUNTRY,
"Country does not match your authentication.");
RevocationBatchServiceException.Reason.INVALID_COUNTRY,
"Country does not match your authentication.");
}
}

Expand Down Expand Up @@ -320,6 +389,17 @@ private void contentCheckValidValuesForDeletion(RevocationBatchDeleteRequestDto
}
}

private void contentCheckMigrateCms(String payload, String entityCms)
throws RevocationBatchServiceException {
SignedStringDto signedStringDto = getSignedString(entityCms);
if (!payload.equals(signedStringDto.getPayloadString())) {
throw new RevocationBatchServiceException(
RevocationBatchServiceException.Reason.INVALID_JSON_VALUES,
"New cms payload does not match present payload."
);
}
}

/**
* Checks a given UploadCertificate if it exists in the database and is assigned to given CountryCode.
*
Expand Down
Loading

0 comments on commit 7c9e0cc

Please sign in to comment.