Skip to content

Commit

Permalink
Certificate API - Certificate import/export support (#535)
Browse files Browse the repository at this point in the history
- Adds certificates to vault backup model (export)
- Adds certificate import and export implementation
- Creates/updates test cases to cover changes
- Removes unnecessary null-check tests
- Updates documentation

Resolves #502
{minor}

Signed-off-by: Esta Nagy <[email protected]>
  • Loading branch information
nagyesta authored Apr 2, 2023
1 parent 9629c77 commit 431b39a
Show file tree
Hide file tree
Showing 33 changed files with 669 additions and 332 deletions.
13 changes: 2 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,6 @@ Lowkey Vault is far from supporting all Azure Key Vault features. The list suppo

### Certificates

![https://img.shields.io/badge/status-experimental-red](https://img.shields.io/badge/status-experimental-red)

- API version supported: ```7.3```
- Create certificate
- Self-signed only
Expand All @@ -169,20 +167,13 @@ Lowkey Vault is far from supporting all Azure Key Vault features. The list suppo
- The downloadable certificate is protected using a blank (`""`) password for `PKCS12` stores
- Get deleted certificate
- Latest version of a single certificate
- List of all certificate
- List of all certificates
- Delete certificate
- Update certificate properties
- Update certificate issuance policy
- Recover deleted certificate
- Purge deleted certificate
- Backup and restore certificate

#### Warning!

Certificate API features are work in progress, many Lowkey Vault features might
not work or are known to be broken, for example but not limited to the following:

- Import and export ignores certificates
- Backup and restore certificates

### Management API

Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,9 @@
package com.github.nagyesta.lowkeyvault.controller;

import com.github.nagyesta.lowkeyvault.controller.v7_3.KeyBackupRestoreController;
import com.github.nagyesta.lowkeyvault.controller.v7_3.SecretBackupRestoreController;
import com.github.nagyesta.lowkeyvault.management.VaultImportExportExecutor;
import com.github.nagyesta.lowkeyvault.model.common.ErrorModel;
import com.github.nagyesta.lowkeyvault.model.common.backup.KeyBackupList;
import com.github.nagyesta.lowkeyvault.model.common.backup.KeyBackupModel;
import com.github.nagyesta.lowkeyvault.model.common.backup.SecretBackupList;
import com.github.nagyesta.lowkeyvault.model.common.backup.SecretBackupModel;
import com.github.nagyesta.lowkeyvault.model.management.VaultBackupListModel;
import com.github.nagyesta.lowkeyvault.model.management.VaultBackupModel;
import com.github.nagyesta.lowkeyvault.model.management.VaultModel;
import com.github.nagyesta.lowkeyvault.service.exception.NotFoundException;
import com.github.nagyesta.lowkeyvault.service.key.ReadOnlyKeyVaultKeyEntity;
import com.github.nagyesta.lowkeyvault.service.key.id.VersionedKeyEntityId;
import com.github.nagyesta.lowkeyvault.service.secret.ReadOnlyKeyVaultSecretEntity;
import com.github.nagyesta.lowkeyvault.service.secret.id.VersionedSecretEntityId;
import com.github.nagyesta.lowkeyvault.service.vault.VaultService;
import com.github.nagyesta.lowkeyvault.template.backup.VaultImporter;
import io.swagger.v3.oas.annotations.Operation;
Expand All @@ -33,13 +22,7 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;

@Slf4j
@RestController
Expand All @@ -50,31 +33,21 @@ public class VaultBackupManagementController extends ErrorHandlingAwareControlle

private final VaultImporter vaultImporter;
private final VaultService vaultService;
private final VaultManagementController vaultManagementController;
private final KeyBackupRestoreController keyBackupRestoreController;
private final SecretBackupRestoreController secretBackupRestoreController;
private final VaultImportExportExecutor vaultImportExportExecutor;

@Autowired
public VaultBackupManagementController(@NonNull final VaultImporter vaultImporter,
@NonNull final VaultService vaultService,
@NonNull final VaultManagementController vaultManagementController,
@NonNull final KeyBackupRestoreController keyBackupRestoreController,
@NonNull final SecretBackupRestoreController secretBackupRestoreController) {
@NonNull final VaultImportExportExecutor vaultImportExportExecutor) {
this.vaultImporter = vaultImporter;
this.vaultService = vaultService;
this.vaultManagementController = vaultManagementController;
this.keyBackupRestoreController = keyBackupRestoreController;
this.secretBackupRestoreController = secretBackupRestoreController;
this.vaultImportExportExecutor = vaultImportExportExecutor;
}

@Override
public void afterPropertiesSet() {
vaultImporter.getVaults().forEach((baseUri, vault) -> {
vaultManagementController.createVault(vault);
vaultImporter.getKeys().getOrDefault(baseUri, Collections.emptyList())
.forEach(key -> keyBackupRestoreController.restore(baseUri, key));
vaultImporter.getSecrets().getOrDefault(baseUri, Collections.emptyList())
.forEach(secret -> secretBackupRestoreController.restore(baseUri, secret));
vaultImportExportExecutor.restoreVault(vaultImporter, baseUri, vault);
});
}

Expand All @@ -94,54 +67,10 @@ public void afterPropertiesSet() {
@GetMapping("/export")
public ResponseEntity<VaultBackupListModel> export() {
log.info("Received request to export active vaults.");
final List<VaultBackupModel> backupModels = Optional.ofNullable(vaultManagementController.listVaults())
.map(ResponseEntity::getBody)
.orElse(Collections.emptyList()).stream()
.map(this::backupVault)
.collect(Collectors.toList());
final List<VaultBackupModel> backupModels = vaultImportExportExecutor.backupVaultList(vaultService);
final VaultBackupListModel vaultBackupListModel = new VaultBackupListModel();
vaultBackupListModel.setVaults(backupModels);
log.info("Export completed.");
return ResponseEntity.ok(vaultBackupListModel);
}

private Map<String, KeyBackupList> mapKeys(final URI baseUri) {
return vaultService.findByUri(baseUri)
.keyVaultFake().getEntities()
.listLatestEntities().stream()
.map(ReadOnlyKeyVaultKeyEntity::getId)
.map(VersionedKeyEntityId::id)
.collect(Collectors.toMap(Function.identity(), name -> backupKey(baseUri, name)));
}

private KeyBackupList backupKey(final URI baseUri, final String name) {
return Optional.ofNullable(keyBackupRestoreController.backup(name, baseUri))
.map(ResponseEntity::getBody)
.map(KeyBackupModel::getValue)
.orElseThrow(() -> new NotFoundException("Key not found: " + name));
}

private Map<String, SecretBackupList> mapSecrets(final URI baseUri) {
return vaultService.findByUri(baseUri)
.secretVaultFake().getEntities()
.listLatestEntities().stream()
.map(ReadOnlyKeyVaultSecretEntity::getId)
.map(VersionedSecretEntityId::id)
.collect(Collectors.toMap(Function.identity(), name -> backupSecret(baseUri, name)));
}

private SecretBackupList backupSecret(final URI baseUri, final String name) {
return Optional.ofNullable(secretBackupRestoreController.backup(name, baseUri))
.map(ResponseEntity::getBody)
.map(SecretBackupModel::getValue)
.orElseThrow(() -> new NotFoundException("Secret not found: " + name));
}

private VaultBackupModel backupVault(final VaultModel vaultModel) {
final VaultBackupModel backupModel = new VaultBackupModel();
backupModel.setAttributes(vaultModel);
backupModel.setKeys(mapKeys(vaultModel.getBaseUri()));
backupModel.setSecrets(mapSecrets(vaultModel.getBaseUri()));
return backupModel;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,16 @@ protected void restoreVersion(@NonNull final CertificateVaultFake vault,
@NonNull final CertificateBackupListItem entityVersion) {
final CertificatePropertiesModel attributes = Objects
.requireNonNullElse(entityVersion.getAttributes(), new CertificatePropertiesModel());
final CertificatePolicyModel issuancePolicy = Optional.ofNullable(entityVersion.getIssuancePolicy())
.orElse(entityVersion.getPolicy());
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())
.issuancePolicy(issuancePolicy)
.tags(entityVersion.getTags())
.created(attributes.getCreatedOn())
.updated(attributes.getUpdatedOn())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
@Slf4j
@RestController
@Validated
@DependsOn("certificateBackupConverter")
@DependsOn({"certificateBackupConverter", "certificateModelConverter"})
@Component("CertificateBackupRestoreControllerV73")
public class CertificateBackupRestoreController extends CommonCertificateBackupRestoreController {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package com.github.nagyesta.lowkeyvault.management;

import com.github.nagyesta.lowkeyvault.controller.VaultManagementController;
import com.github.nagyesta.lowkeyvault.controller.v7_3.CertificateBackupRestoreController;
import com.github.nagyesta.lowkeyvault.controller.v7_3.KeyBackupRestoreController;
import com.github.nagyesta.lowkeyvault.controller.v7_3.SecretBackupRestoreController;
import com.github.nagyesta.lowkeyvault.model.common.backup.*;
import com.github.nagyesta.lowkeyvault.model.management.VaultBackupModel;
import com.github.nagyesta.lowkeyvault.model.management.VaultModel;
import com.github.nagyesta.lowkeyvault.service.certificate.ReadOnlyKeyVaultCertificateEntity;
import com.github.nagyesta.lowkeyvault.service.certificate.id.VersionedCertificateEntityId;
import com.github.nagyesta.lowkeyvault.service.exception.NotFoundException;
import com.github.nagyesta.lowkeyvault.service.key.ReadOnlyKeyVaultKeyEntity;
import com.github.nagyesta.lowkeyvault.service.key.id.VersionedKeyEntityId;
import com.github.nagyesta.lowkeyvault.service.secret.ReadOnlyKeyVaultSecretEntity;
import com.github.nagyesta.lowkeyvault.service.secret.id.VersionedSecretEntityId;
import com.github.nagyesta.lowkeyvault.service.vault.VaultFake;
import com.github.nagyesta.lowkeyvault.service.vault.VaultService;
import com.github.nagyesta.lowkeyvault.template.backup.VaultImporter;
import lombok.NonNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;

import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;

@Component
public class VaultImportExportExecutor {

private final VaultManagementController vaultManagementController;
private final KeyBackupRestoreController keyBackupRestoreController;
private final SecretBackupRestoreController secretBackupRestoreController;
private final CertificateBackupRestoreController certificateBackupRestoreController;

@Autowired
public VaultImportExportExecutor(@NonNull final VaultManagementController vaultManagementController,
@NonNull final KeyBackupRestoreController keyBackupRestoreController,
@NonNull final SecretBackupRestoreController secretBackupRestoreController,
@NonNull final CertificateBackupRestoreController certificateBackupRestoreController) {
this.vaultManagementController = vaultManagementController;
this.keyBackupRestoreController = keyBackupRestoreController;
this.secretBackupRestoreController = secretBackupRestoreController;
this.certificateBackupRestoreController = certificateBackupRestoreController;
}

public void restoreVault(@NonNull final VaultImporter vaultImporter, @NonNull final URI baseUri, @NonNull final VaultModel vault) {
vaultManagementController.createVault(vault);
vaultImporter.getKeys().getOrDefault(baseUri, Collections.emptyList())
.forEach(key -> keyBackupRestoreController.restore(baseUri, key));
vaultImporter.getSecrets().getOrDefault(baseUri, Collections.emptyList())
.forEach(secret -> secretBackupRestoreController.restore(baseUri, secret));
vaultImporter.getCertificates().getOrDefault(baseUri, Collections.emptyList())
.forEach(certificate -> certificateBackupRestoreController.restore(baseUri, certificate));
}

public List<VaultBackupModel> backupVaultList(@NonNull final VaultService vaultService) {
return Optional.ofNullable(vaultManagementController.listVaults())
.map(ResponseEntity::getBody)
.orElse(Collections.emptyList()).stream()
.map(vaultModel -> backupVault(vaultService, vaultModel))
.collect(Collectors.toList());
}

private KeyBackupList backupKey(final URI baseUri, final String name) {
return Optional.ofNullable(keyBackupRestoreController.backup(name, baseUri))
.map(ResponseEntity::getBody)
.map(KeyBackupModel::getValue)
.orElseThrow(() -> new NotFoundException("Key not found: " + name));
}

private SecretBackupList backupSecret(final URI baseUri, final String name) {
return Optional.ofNullable(secretBackupRestoreController.backup(name, baseUri))
.map(ResponseEntity::getBody)
.map(SecretBackupModel::getValue)
.orElseThrow(() -> new NotFoundException("Secret not found: " + name));
}

private CertificateBackupList backupCertificate(final URI baseUri, final String name) {
return Optional.ofNullable(certificateBackupRestoreController.backup(name, baseUri))
.map(ResponseEntity::getBody)
.map(CertificateBackupModel::getValue)
.orElseThrow(() -> new NotFoundException("Certificate not found: " + name));
}

private VaultBackupModel backupVault(final VaultService vaultService, final VaultModel vaultModel) {
final VaultFake vaultFake = vaultService.findByUri(vaultModel.getBaseUri());
final VaultBackupModel backupModel = new VaultBackupModel();
backupModel.setAttributes(vaultModel);
backupModel.setKeys(mapKeys(vaultFake));
backupModel.setSecrets(mapSecrets(vaultFake));
backupModel.setCertificates(mapCertificates(vaultFake));
return backupModel;
}

private Map<String, KeyBackupList> mapKeys(final VaultFake vaultFake) {
return vaultFake.keyVaultFake().getEntities()
.listLatestEntities().stream()
//exclude managed entities as certificates will take care of those
.filter(r -> !r.isManaged())
.map(ReadOnlyKeyVaultKeyEntity::getId)
.map(VersionedKeyEntityId::id)
.collect(Collectors.toMap(Function.identity(), name -> backupKey(vaultFake.baseUri(), name)));
}

private Map<String, SecretBackupList> mapSecrets(final VaultFake vaultFake) {
return vaultFake.secretVaultFake().getEntities()
.listLatestEntities().stream()
//exclude managed entities as certificates will take care of those
.filter(r -> !r.isManaged())
.map(ReadOnlyKeyVaultSecretEntity::getId)
.map(VersionedSecretEntityId::id)
.collect(Collectors.toMap(Function.identity(), name -> backupSecret(vaultFake.baseUri(), name)));
}

private Map<String, CertificateBackupList> mapCertificates(final VaultFake vaultFake) {
return vaultFake.certificateVaultFake().getEntities()
.listLatestEntities().stream()
.map(ReadOnlyKeyVaultCertificateEntity::getId)
.map(VersionedCertificateEntityId::id)
.collect(Collectors.toMap(Function.identity(), name -> backupCertificate(vaultFake.baseUri(), name)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.github.nagyesta.lowkeyvault.model.common.backup.CertificateBackupList;
import com.github.nagyesta.lowkeyvault.model.common.backup.KeyBackupList;
import com.github.nagyesta.lowkeyvault.model.common.backup.SecretBackupList;
import lombok.Data;
Expand All @@ -18,11 +19,12 @@ public class VaultBackupModel {
@JsonProperty("attributes")
private VaultModel attributes;
@Valid
@NotNull
@JsonProperty("keys")
private Map<String, KeyBackupList> keys;
@Valid
@NotNull
@JsonProperty("secrets")
private Map<String, SecretBackupList> secrets;
@Valid
@JsonProperty("certificates")
private Map<String, CertificateBackupList> certificates;
}
Loading

0 comments on commit 431b39a

Please sign in to comment.