Skip to content

Commit

Permalink
Certificate API - Certificate backup/restore (#528)
Browse files Browse the repository at this point in the history
- Implements certificate restore functionality in service
- Adds certificate restore input model
- Creates base backup and restore controller for certificates
- Creates converter for certificate backup
- Adds new UT/IT test cases
- Adds new E2E test cases

Resolves #500
{minor}

Signed-off-by: Esta Nagy <[email protected]>
  • Loading branch information
nagyesta authored Mar 28, 2023
1 parent 2ebd281 commit 6573c9b
Show file tree
Hide file tree
Showing 28 changed files with 1,122 additions and 59 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ Lowkey Vault is far from supporting all Azure Key Vault features. The list suppo
- Update certificate issuance policy
- Recover deleted certificate
- Purge deleted certificate
- Backup and restore certificate

#### Warning!

Expand Down
5 changes: 1 addition & 4 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,7 @@ configure(subprojects.findAll({
'com.github.nagyesta.lowkeyvault.service.exception.AlreadyExistsException',
'com.github.nagyesta.lowkeyvault.service.exception.NotFoundException',
'com.github.nagyesta.lowkeyvault.service.exception.CryptoException',
'com.github.nagyesta.lowkeyvault.exception.VaultNotFoundException',
//until Certificate backup features are implemented
'com.github.nagyesta.lowkeyvault.model.common.backup.CertificateBackupModel',
'com.github.nagyesta.lowkeyvault.model.common.backup.CertificateBackupList'
'com.github.nagyesta.lowkeyvault.exception.VaultNotFoundException'
]
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@

import com.github.nagyesta.lowkeyvault.mapper.common.registry.CertificateConverterRegistry;
import com.github.nagyesta.lowkeyvault.mapper.v7_3.certificate.*;
import com.github.nagyesta.lowkeyvault.service.vault.VaultService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;

@Configuration
public class CertificateConverterConfiguration {

@Autowired
private VaultService vaultService;

@Bean
public CertificateConverterRegistry certificateConverterRegistry() {
return new CertificateConverterRegistry();
Expand Down Expand Up @@ -56,4 +61,11 @@ public CertificateEntityToV73PendingCertificateOperationModelConverter certifica
public CertificateLifetimeActionsPolicyToV73ModelConverter certificateLifetimeActionConverter() {
return new CertificateLifetimeActionsPolicyToV73ModelConverter(certificateConverterRegistry());
}

@Bean
@DependsOn({"certificatePropertiesConverter", "certificatePolicyConverter", "certificateIssuancePolicyConverter",
"certificateLifetimeActionConverter"})
public CertificateEntityToV73BackupConverter certificateBackupConverter() {
return new CertificateEntityToV73BackupConverter(certificateConverterRegistry(), vaultService);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package com.github.nagyesta.lowkeyvault.controller.common;

import com.github.nagyesta.lowkeyvault.mapper.common.registry.CertificateConverterRegistry;
import com.github.nagyesta.lowkeyvault.mapper.v7_3.certificate.CertificateEntityToV73CertificateItemModelConverter;
import com.github.nagyesta.lowkeyvault.mapper.v7_3.certificate.CertificateEntityToV73CertificateVersionItemModelConverter;
import com.github.nagyesta.lowkeyvault.mapper.v7_3.certificate.CertificateEntityToV73ModelConverter;
import com.github.nagyesta.lowkeyvault.model.common.backup.CertificateBackupList;
import com.github.nagyesta.lowkeyvault.model.common.backup.CertificateBackupListItem;
import com.github.nagyesta.lowkeyvault.model.common.backup.CertificateBackupModel;
import com.github.nagyesta.lowkeyvault.model.v7_3.certificate.*;
import com.github.nagyesta.lowkeyvault.service.certificate.CertificateVaultFake;
import com.github.nagyesta.lowkeyvault.service.certificate.ReadOnlyKeyVaultCertificateEntity;
import com.github.nagyesta.lowkeyvault.service.certificate.id.CertificateEntityId;
import com.github.nagyesta.lowkeyvault.service.certificate.id.VersionedCertificateEntityId;
import com.github.nagyesta.lowkeyvault.service.certificate.impl.CertAuthorityType;
import com.github.nagyesta.lowkeyvault.service.certificate.impl.CertContentType;
import com.github.nagyesta.lowkeyvault.service.certificate.impl.CertificateLifetimeActionPolicy;
import com.github.nagyesta.lowkeyvault.service.certificate.impl.DefaultCertificateLifetimeActionPolicy;
import com.github.nagyesta.lowkeyvault.service.vault.VaultFake;
import com.github.nagyesta.lowkeyvault.service.vault.VaultService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.NonNull;

import javax.validation.Valid;
import javax.validation.constraints.Pattern;
import java.net.URI;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

import static com.github.nagyesta.lowkeyvault.controller.common.util.CertificateRequestMapperUtil.convertActivityMap;

@Slf4j
public abstract class CommonCertificateBackupRestoreController extends BaseBackupRestoreController<CertificateEntityId,
VersionedCertificateEntityId, ReadOnlyKeyVaultCertificateEntity, KeyVaultCertificateModel, DeletedKeyVaultCertificateModel,
KeyVaultCertificateItemModel, DeletedKeyVaultCertificateItemModel, CertificateEntityToV73ModelConverter,
CertificateEntityToV73CertificateItemModelConverter, CertificateEntityToV73CertificateVersionItemModelConverter,
CertificateVaultFake, CertificatePropertiesModel, CertificateBackupListItem, CertificateBackupList, CertificateBackupModel,
CertificateConverterRegistry> {

protected CommonCertificateBackupRestoreController(
@NonNull final CertificateConverterRegistry registry, @NonNull final VaultService vaultService) {
super(registry, vaultService, VaultFake::certificateVaultFake);
}

public ResponseEntity<CertificateBackupModel> backup(
@Valid @Pattern(regexp = NAME_PATTERN) final String certificateName,
final URI baseUri) {
log.info("Received request to {} backup certificate: {} using API version: {}",
baseUri.toString(), certificateName, apiVersion());
return ResponseEntity.ok(backupEntity(entityId(baseUri, certificateName)));
}

public ResponseEntity<KeyVaultCertificateModel> restore(
final URI baseUri, @Valid final CertificateBackupModel certificateBackupModel) {
final CertificateBackupList list = certificateBackupModel.getValue();
log.info("Received request to {} restore certificate: {} using API version: {}",
baseUri.toString(), list.getVersions().get(0).getId(), apiVersion());
final KeyVaultCertificateModel model = restoreEntity(certificateBackupModel);
final CertificateVaultFake vault = getVaultByUri(baseUri);
final CertificateEntityId entityId = entityId(baseUri, getSingleEntityName(certificateBackupModel));
model.getPolicy().setLifetimeActions(updateLifetimeActions(vault, entityId, list));
return ResponseEntity.ok(model);
}

@Override
protected void restoreVersion(@NonNull final CertificateVaultFake vault,
@NonNull final VersionedCertificateEntityId versionedEntityId,
@NonNull final CertificateBackupListItem entityVersion) {
final CertificatePropertiesModel attributes = Objects
.requireNonNullElse(entityVersion.getAttributes(), new CertificatePropertiesModel());
vault.restoreCertificateVersion(versionedEntityId, CertificateRestoreInput.builder()
.name(versionedEntityId.id())
.certificateContent(entityVersion.getCertificateAsString())
.keyVersion(entityVersion.getKeyVersion())
.contentType(CertContentType.byMimeType(entityVersion.getPolicy().getSecretProperties().getContentType()))
.password(entityVersion.getPassword())
.policy(entityVersion.getPolicy())
.issuancePolicy(entityVersion.getIssuancePolicy())
.tags(entityVersion.getTags())
.created(attributes.getCreatedOn())
.updated(attributes.getUpdatedOn())
.notBefore(attributes.getNotBefore())
.expires(attributes.getExpiresOn())
.enabled(attributes.isEnabled())
.build());
}

@Override
protected CertificateBackupList getBackupList() {
return new CertificateBackupList();
}

@Override
protected CertificateBackupModel getBackupModel() {
return new CertificateBackupModel();
}

private List<CertificateLifetimeActionModel> updateLifetimeActions(
final CertificateVaultFake vault, final CertificateEntityId entityId, final CertificateBackupList list) {
final VersionedCertificateEntityId latestVersion = vault.getEntities().getLatestVersionOfEntity(entityId);
final CertAuthorityType certAuthorityType = vault.getEntities().getReadOnlyEntity(latestVersion)
.getIssuancePolicy().getCertAuthorityType();
final CertificateLifetimeActionPolicy lifetimeActionPolicy = Optional.ofNullable(list.getVersions())
.map(v -> v.get(v.size() - 1))
.map(CertificateBackupListItem::getPolicy)
.map(CertificatePolicyModel::getLifetimeActions)
.map(actions -> new CertificateLifetimeActionPolicy(entityId, convertActivityMap(actions)))
.orElse(new DefaultCertificateLifetimeActionPolicy(entityId, certAuthorityType));
vault.setLifetimeActionPolicy(lifetimeActionPolicy);
return registry().lifetimeActionConverters(apiVersion()).convert(vault.lifetimeActionPolicy(entityId));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.github.nagyesta.lowkeyvault.service.certificate.impl.*;
import org.springframework.util.Assert;

import java.nio.charset.StandardCharsets;
import java.time.OffsetDateTime;
import java.util.*;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -62,41 +63,15 @@ public static void updateIssuancePolicy(
setLifetimeActions(vault, request, entityId, entity.getOriginalCertificatePolicy().getCertAuthorityType());
}

private static CertificatePropertiesModel defaultIfNull(final CertificatePropertiesModel model) {
return Objects.requireNonNullElse(model, new CertificatePropertiesModel());
}

private static void validateLifetimeActions(final CertificatePolicyModel policy) {
final Integer validityMonths = Objects.requireNonNullElse(policy.getX509Properties().getValidityMonths(),
DEFAULT_VALIDITY_MONTHS);
Optional.ofNullable(policy.getLifetimeActions())
.ifPresent(actions -> actions.forEach(a -> a.getTrigger().validate(validityMonths)));
}

private static void setLifetimeActions(
final CertificateVaultFake certificateVaultFake,
final CertificatePolicyModel policy,
final VersionedCertificateEntityId certificateEntityId,
final CertAuthorityType certAuthorityType) {
final CertificateLifetimeActionPolicy lifetimeActionPolicy = Optional.ofNullable(policy.getLifetimeActions())
.map(actions -> new CertificateLifetimeActionPolicy(certificateEntityId, convertActivityMap(actions)))
.orElse(new DefaultCertificateLifetimeActionPolicy(certificateEntityId, certAuthorityType));
certificateVaultFake.setLifetimeActionPolicy(lifetimeActionPolicy);
}

private static Map<CertificateLifetimeActionActivity, CertificateLifetimeActionTrigger> convertActivityMap(
final List<CertificateLifetimeActionModel> actions) {
return actions.stream().collect(Collectors
.toMap(CertificateLifetimeActionModel::getAction, c -> c.getTrigger().asTriggerEntity()));
}

private static CertificateCreationInput toCertificateCreationInput(
final String certificateName, final CreateCertificateRequest request) {
final CertificatePolicyModel policy = request.getPolicy();
return convertPolicyToCertificateCreationInput(certificateName, policy);
public static String getCertificateAsString(final byte[] certificate) {
String value = new String(certificate, StandardCharsets.UTF_8);
if (!value.contains("BEGIN")) {
value = Base64.getMimeEncoder().encodeToString(certificate);
}
return value;
}

private static CertificateCreationInput convertPolicyToCertificateCreationInput(
public static CertificateCreationInput convertPolicyToCertificateCreationInput(
final String certificateName, final CertificatePolicyModel policy) {
final X509CertificateModel x509Properties = policy.getX509Properties();
final IssuerParameterModel issuer = policy.getIssuer();
Expand Down Expand Up @@ -128,6 +103,40 @@ private static CertificateCreationInput convertPolicyToCertificateCreationInput(
.build();
}

public static Map<CertificateLifetimeActionActivity, CertificateLifetimeActionTrigger> convertActivityMap(
final List<CertificateLifetimeActionModel> actions) {
return actions.stream().collect(Collectors
.toMap(CertificateLifetimeActionModel::getAction, c -> c.getTrigger().asTriggerEntity()));
}

private static CertificatePropertiesModel defaultIfNull(final CertificatePropertiesModel model) {
return Objects.requireNonNullElse(model, new CertificatePropertiesModel());
}

private static void validateLifetimeActions(final CertificatePolicyModel policy) {
final Integer validityMonths = Objects.requireNonNullElse(policy.getX509Properties().getValidityMonths(),
DEFAULT_VALIDITY_MONTHS);
Optional.ofNullable(policy.getLifetimeActions())
.ifPresent(actions -> actions.forEach(a -> a.getTrigger().validate(validityMonths)));
}

private static void setLifetimeActions(
final CertificateVaultFake certificateVaultFake,
final CertificatePolicyModel policy,
final VersionedCertificateEntityId certificateEntityId,
final CertAuthorityType certAuthorityType) {
final CertificateLifetimeActionPolicy lifetimeActionPolicy = Optional.ofNullable(policy.getLifetimeActions())
.map(actions -> new CertificateLifetimeActionPolicy(certificateEntityId, convertActivityMap(actions)))
.orElse(new DefaultCertificateLifetimeActionPolicy(certificateEntityId, certAuthorityType));
certificateVaultFake.setLifetimeActionPolicy(lifetimeActionPolicy);
}

private static CertificateCreationInput toCertificateCreationInput(
final String certificateName, final CreateCertificateRequest request) {
final CertificatePolicyModel policy = request.getPolicy();
return convertPolicyToCertificateCreationInput(certificateName, policy);
}

private static CertificateImportInput toCertificateImportInput(
final String certificateName, final CertificateImportRequest request) {
final CertificatePolicyModel policyModel = Objects.requireNonNullElse(request.getPolicy(), new CertificatePolicyModel());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.github.nagyesta.lowkeyvault.controller.v7_3;

import com.github.nagyesta.lowkeyvault.controller.common.CommonCertificateBackupRestoreController;
import com.github.nagyesta.lowkeyvault.mapper.common.registry.CertificateConverterRegistry;
import com.github.nagyesta.lowkeyvault.model.common.ApiConstants;
import com.github.nagyesta.lowkeyvault.model.common.backup.CertificateBackupModel;
import com.github.nagyesta.lowkeyvault.model.v7_3.certificate.KeyVaultCertificateModel;
import com.github.nagyesta.lowkeyvault.service.vault.VaultService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.DependsOn;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import javax.validation.constraints.Pattern;
import java.net.URI;

import static com.github.nagyesta.lowkeyvault.model.common.ApiConstants.API_VERSION_7_3;
import static com.github.nagyesta.lowkeyvault.model.common.ApiConstants.V_7_3;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;

@Slf4j
@RestController
@Validated
@DependsOn("certificateBackupConverter")
@Component("CertificateBackupRestoreControllerV73")
public class CertificateBackupRestoreController extends CommonCertificateBackupRestoreController {

@Autowired
public CertificateBackupRestoreController(
@NonNull final CertificateConverterRegistry registry, @NonNull final VaultService vaultService) {
super(registry, vaultService);
}

@Override
@PostMapping(value = "/certificates/{certificateName}/backup",
params = API_VERSION_7_3,
produces = APPLICATION_JSON_VALUE)
public ResponseEntity<CertificateBackupModel> backup(@PathVariable @Valid @Pattern(regexp = NAME_PATTERN) final String certificateName,
@RequestAttribute(name = ApiConstants.REQUEST_BASE_URI) final URI baseUri) {
return super.backup(certificateName, baseUri);
}

@Override
@PostMapping(value = "/certificates/restore",
params = API_VERSION_7_3,
consumes = APPLICATION_JSON_VALUE,
produces = APPLICATION_JSON_VALUE)
public ResponseEntity<KeyVaultCertificateModel> restore(@RequestAttribute(name = ApiConstants.REQUEST_BASE_URI) final URI baseUri,
@Valid @RequestBody final CertificateBackupModel certificateBackupModel) {
return super.restore(baseUri, certificateBackupModel);
}

@Override
protected String apiVersion() {
return V_7_3;
}
}
Loading

0 comments on commit 6573c9b

Please sign in to comment.