Skip to content

Commit

Permalink
Certificate API - Delete/recover/purge certificates (#487)
Browse files Browse the repository at this point in the history
- Adds new certificate controller endpoint for delete certificate
- Adds new certificate controller endpoint for recover certificate
- Adds new certificate controller endpoint for purge certificate
- Adds new certificate controller endpoint for get deleted certificate
- Adds new certificate controller endpoint for get deleted certificates
- Adds new certificate controller endpoint for pending deleted operation
- Minor refactoring in related code of key/secret controllers
- Changes CertificateVaultFake to forward delete/purge/recover calls to the managed entities
- Adds new tests to cover new CertificateVaultFake features
- Adds new integration tests to cover new CertificateController endpoints
- Adds new end-to-end tests to cover new CertificateController endpoints
- Removes recovery specific information from certificate policy models
- Updates documentation

Resolves #478
{minor}

Signed-off-by: Esta Nagy <nagyesta@gmail.com>
  • Loading branch information
nagyesta authored Mar 1, 2023
1 parent c6964d1 commit a1bc43b
Show file tree
Hide file tree
Showing 23 changed files with 838 additions and 97 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ Lowkey Vault is far from supporting all Azure Key Vault features. The list suppo
- Password used for PKCS12 stores: `lowkey-vault`
- Get certificate operation
- Get pending create operation results
- Get pending delete operation results
- Get available certificate versions
- Get certificate
- Latest version of a single certificate
Expand All @@ -164,6 +165,12 @@ Lowkey Vault is far from supporting all Azure Key Vault features. The list suppo
- Import certificate
- Self-signed only
- The downloadable certificate is protected using `lowkey-vault` as password for PKCS12 stores
- Get deleted certificate
- Latest version of a single certificate
- List of all certificate
- Delete certificate
- Recover deleted certificate
- Purge deleted certificate

#### Warning!

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,23 @@ public ResponseEntity<KeyVaultPendingCertificateModel> pendingCreate(
log.info("Received request to {} get pending create certificate: {} using API version: {}",
baseUri.toString(), certificateName, apiVersion());
final CertificateVaultFake vaultFake = getVaultByUri(baseUri);
final VersionedCertificateEntityId entityId = vaultFake.getEntities().getLatestVersionOfEntity(entityId(baseUri, certificateName));
final ReadOnlyKeyVaultCertificateEntity readOnlyEntity = vaultFake.getEntities().getReadOnlyEntity(entityId);
final VersionedCertificateEntityId entityId = vaultFake
.getEntities().getLatestVersionOfEntity(entityId(baseUri, certificateName));
final ReadOnlyKeyVaultCertificateEntity readOnlyEntity = vaultFake
.getEntities().getReadOnlyEntity(entityId);
return ResponseEntity.ok(pendingModelConverter.convert(readOnlyEntity, baseUri));
}

public ResponseEntity<KeyVaultPendingCertificateModel> pendingDelete(
@Valid @Pattern(regexp = NAME_PATTERN) final String certificateName,
final URI baseUri) {
log.info("Received request to {} get pending delete certificate: {} using API version: {}",
baseUri.toString(), certificateName, apiVersion());
final CertificateVaultFake vaultFake = getVaultByUri(baseUri);
final VersionedCertificateEntityId entityId = vaultFake.getDeletedEntities()
.getLatestVersionOfEntity(entityId(baseUri, certificateName));
final ReadOnlyKeyVaultCertificateEntity readOnlyEntity = vaultFake
.getDeletedEntities().getReadOnlyEntity(entityId);
return ResponseEntity.ok(pendingModelConverter.convert(readOnlyEntity, baseUri));
}

Expand All @@ -90,6 +105,7 @@ public ResponseEntity<KeyVaultCertificateModel> get(

return ResponseEntity.ok(getLatestEntityModel(baseUri, certificateName));
}

public ResponseEntity<CertificatePolicyModel> getPolicy(
@Valid @Pattern(regexp = NAME_PATTERN) final String certificateName,
final URI baseUri) {
Expand Down Expand Up @@ -122,6 +138,56 @@ public ResponseEntity<KeyVaultCertificateModel> importCertificate(
return ResponseEntity.ok().body(convertDetails(readOnlyEntity, baseUri));
}

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

final CertificateVaultFake vaultFake = getVaultByUri(baseUri);
final CertificateEntityId entityId = new CertificateEntityId(baseUri, certificateName);
vaultFake.delete(entityId);
final VersionedCertificateEntityId latestVersion = vaultFake.getDeletedEntities().getLatestVersionOfEntity(entityId);
return ResponseEntity.ok(getDeletedModelById(vaultFake, latestVersion, baseUri, true));
}

public ResponseEntity<DeletedKeyVaultCertificateModel> getDeletedCertificate(
@Valid @Pattern(regexp = NAME_PATTERN) final String certificateName,
final URI baseUri) {
log.info("Received request to {} get deleted certificate: {} using API version: {}",
baseUri.toString(), certificateName, apiVersion());

final CertificateVaultFake vaultFake = getVaultByUri(baseUri);
final CertificateEntityId entityId = new CertificateEntityId(baseUri, certificateName);
final VersionedCertificateEntityId latestVersion = vaultFake.getDeletedEntities().getLatestVersionOfEntity(entityId);
return ResponseEntity.ok(getDeletedModelById(vaultFake, latestVersion, baseUri, false));
}

public ResponseEntity<KeyVaultCertificateModel> recoverDeletedCertificate(
@Valid @Pattern(regexp = NAME_PATTERN) final String certificateName,
final URI baseUri) {
log.info("Received request to {} recover deleted certificate: {} using API version: {}",
baseUri.toString(), certificateName, apiVersion());

final CertificateVaultFake vaultFake = getVaultByUri(baseUri);
final CertificateEntityId entityId = new CertificateEntityId(baseUri, certificateName);
vaultFake.recover(entityId);
final VersionedCertificateEntityId latestVersion = vaultFake.getEntities().getLatestVersionOfEntity(entityId);
return ResponseEntity.ok(getModelById(vaultFake, latestVersion, baseUri, true));
}

public ResponseEntity<Void> purgeDeleted(
@Valid @Pattern(regexp = NAME_PATTERN) final String certificateName,
final URI baseUri) {
log.info("Received request to {} purge deleted certificate: {} using API version: {}",
baseUri.toString(), certificateName, apiVersion());

final CertificateVaultFake vaultFake = getVaultByUri(baseUri);
final CertificateEntityId entityId = new CertificateEntityId(baseUri, certificateName);
vaultFake.purge(entityId);
return ResponseEntity.noContent().build();
}

public ResponseEntity<KeyVaultItemListModel<KeyVaultCertificateItemModel>> versions(
@Valid @Pattern(regexp = NAME_PATTERN) final String certificateName,
final URI baseUri,
Expand All @@ -145,14 +211,29 @@ public ResponseEntity<KeyVaultItemListModel<KeyVaultCertificateItemModel>> listC
return ResponseEntity.ok(getPageOfItems(baseUri, maxResults, skipToken, includePending));
}

public ResponseEntity<KeyVaultItemListModel<DeletedKeyVaultCertificateItemModel>> listDeletedCertificates(
final URI baseUri,
final int maxResults,
final int skipToken,
final boolean includePending) {
log.info("Received request to {} list deleted certificates, (max results: {}, skip: {}, includePending: {}) using API version: {}",
baseUri.toString(), maxResults, skipToken, includePending, apiVersion());

return ResponseEntity.ok(getPageOfDeletedItems(baseUri, maxResults, skipToken, includePending));
}

private KeyVaultItemListModel<KeyVaultCertificateItemModel> getPageOfItems(
final URI baseUri, final int limit, final int offset, final boolean includePending) {
final KeyVaultItemListModel<KeyVaultCertificateItemModel> page = super.getPageOfItems(baseUri, limit, offset, "/certificates");
final String nextLink = Optional.ofNullable(page.getNextLink())
.map(next -> next + "&" + INCLUDE_PENDING_PARAM + "=" + includePending)
.orElse(null);
page.setNextLink(nextLink);
return page;
final KeyVaultItemListModel<KeyVaultCertificateItemModel> page =
super.getPageOfItems(baseUri, limit, offset, "/certificates");
return fixNextLink(page, includePending);
}

private KeyVaultItemListModel<DeletedKeyVaultCertificateItemModel> getPageOfDeletedItems(
final URI baseUri, final int limit, final int offset, final boolean includePending) {
final KeyVaultItemListModel<DeletedKeyVaultCertificateItemModel> page =
super.getPageOfDeletedItems(baseUri, limit, offset, "/deletedcertificates");
return fixNextLink(page, includePending);
}

@Override
Expand All @@ -165,6 +246,16 @@ protected CertificateEntityId entityId(final URI baseUri, final String name) {
return new CertificateEntityId(baseUri, name);
}

private <LI> KeyVaultItemListModel<LI> fixNextLink(
final KeyVaultItemListModel<LI> page,
final boolean includePending) {
final String nextLink = Optional.ofNullable(page.getNextLink())
.map(next -> next + "&" + INCLUDE_PENDING_PARAM + "=" + includePending)
.orElse(null);
page.setNextLink(nextLink);
return page;
}

private VersionedCertificateEntityId createCertificateWithAttributes(
final CertificateVaultFake certificateVaultFake, final String certificateName, final CreateCertificateRequest request) {
final CertificatePropertiesModel properties = Objects.requireNonNullElse(request.getProperties(), new CertificatePropertiesModel());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ public ResponseEntity<KeyVaultItemListModel<KeyVaultKeyItemModel>> listKeys(
return ResponseEntity.ok(getPageOfItems(baseUri, maxResults, skipToken, "/keys"));
}

public ResponseEntity<KeyVaultItemListModel<KeyVaultKeyItemModel>> listDeletedKeys(
public ResponseEntity<KeyVaultItemListModel<DeletedKeyVaultKeyItemModel>> listDeletedKeys(
final URI baseUri,
final int maxResults,
final int skipToken) {
Expand Down Expand Up @@ -145,7 +145,7 @@ public ResponseEntity<KeyVaultKeyModel> updateVersion(
return ResponseEntity.ok(getModelById(keyVaultFake, entityId, baseUri, true));
}

public ResponseEntity<KeyVaultKeyModel> getDeletedKey(
public ResponseEntity<DeletedKeyVaultKeyModel> getDeletedKey(
@Valid @Pattern(regexp = NAME_PATTERN) final String keyName,
final URI baseUri) {
log.info("Received request to {} get deleted key: {} using API version: {}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public ResponseEntity<KeyVaultSecretModel> create(
return ResponseEntity.ok(getModelById(secretVaultFake, secretEntityId, baseUri, true));
}

public ResponseEntity<KeyVaultSecretModel> delete(
public ResponseEntity<DeletedKeyVaultSecretModel> delete(
@Valid @Pattern(regexp = NAME_PATTERN) final String secretName,
final URI baseUri) {
log.info("Received request to {} delete secret: {} using API version: {}",
Expand Down Expand Up @@ -84,7 +84,7 @@ public ResponseEntity<KeyVaultItemListModel<KeyVaultSecretItemModel>> listSecret
return ResponseEntity.ok(getPageOfItems(baseUri, maxResults, skipToken, "/secrets"));
}

public ResponseEntity<KeyVaultItemListModel<KeyVaultSecretItemModel>> listDeletedSecrets(
public ResponseEntity<KeyVaultItemListModel<DeletedKeyVaultSecretItemModel>> listDeletedSecrets(
final URI baseUri,
final int maxResults,
final int skipToken) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,10 @@ protected KeyVaultItemListModel<I> getPageOfItems(final URI baseUri, final int l
}

@SuppressWarnings("SameParameterValue")
protected KeyVaultItemListModel<I> getPageOfDeletedItems(final URI baseUri, final int limit, final int offset, final String uriPath) {
protected KeyVaultItemListModel<DI> getPageOfDeletedItems(final URI baseUri, final int limit, final int offset, final String uriPath) {
final S entityVaultFake = getVaultByUri(baseUri);
final List<E> allItems = entityVaultFake.getDeletedEntities().listLatestNonManagedEntities();
final List<I> items = filterList(limit, offset, allItems, source -> itemConverter.convertDeleted(source, baseUri));
final List<DI> items = filterList(limit, offset, allItems, source -> itemConverter.convertDeleted(source, baseUri));
final URI nextUri = getNextUri(baseUri + uriPath, allItems, items, limit, offset);
return listModel(items, nextUri);
}
Expand Down Expand Up @@ -156,7 +156,7 @@ protected void updateTags(final BaseVaultFake<K, V, ?> vaultFake, final V entity
});
}

protected KeyVaultItemListModel<I> listModel(final List<I> items, final URI nextUri) {
protected <LI> KeyVaultItemListModel<LI> listModel(final List<LI> items, final URI nextUri) {
return new KeyVaultItemListModel<>(items, nextUri);
}

Expand All @@ -169,8 +169,8 @@ private URI getNextUri(final String prefix, final Collection<?> allItems,
return nextUri;
}

private <FR> List<I> filterList(
final int limit, final int offset, final Collection<FR> allItems, final Function<FR, I> mapper) {
private <FR, LI> List<LI> filterList(
final int limit, final int offset, final Collection<FR> allItems, final Function<FR, LI> mapper) {
return allItems.stream()
.skip(offset)
.limit(limit)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import com.github.nagyesta.lowkeyvault.mapper.v7_2.key.KeyEntityToV72ModelConverter;
import com.github.nagyesta.lowkeyvault.model.common.ApiConstants;
import com.github.nagyesta.lowkeyvault.model.common.KeyVaultItemListModel;
import com.github.nagyesta.lowkeyvault.model.v7_2.key.DeletedKeyVaultKeyItemModel;
import com.github.nagyesta.lowkeyvault.model.v7_2.key.DeletedKeyVaultKeyModel;
import com.github.nagyesta.lowkeyvault.model.v7_2.key.KeyVaultKeyItemModel;
import com.github.nagyesta.lowkeyvault.model.v7_2.key.KeyVaultKeyModel;
import com.github.nagyesta.lowkeyvault.model.v7_2.key.request.CreateKeyRequest;
Expand Down Expand Up @@ -48,9 +50,10 @@ public KeyController(@NonNull final KeyEntityToV72ModelConverter keyEntityToV72M
params = API_VERSION_7_2,
consumes = APPLICATION_JSON_VALUE,
produces = APPLICATION_JSON_VALUE)
public ResponseEntity<KeyVaultKeyModel> create(@PathVariable @Valid @Pattern(regexp = NAME_PATTERN) final String keyName,
@RequestAttribute(name = ApiConstants.REQUEST_BASE_URI) final URI baseUri,
@Valid @RequestBody final CreateKeyRequest request) {
public ResponseEntity<KeyVaultKeyModel> create(
@PathVariable @Valid @Pattern(regexp = NAME_PATTERN) final String keyName,
@RequestAttribute(name = ApiConstants.REQUEST_BASE_URI) final URI baseUri,
@Valid @RequestBody final CreateKeyRequest request) {
return super.create(keyName, baseUri, request);
}

Expand All @@ -59,18 +62,20 @@ public ResponseEntity<KeyVaultKeyModel> create(@PathVariable @Valid @Pattern(reg
params = API_VERSION_7_2,
consumes = APPLICATION_JSON_VALUE,
produces = APPLICATION_JSON_VALUE)
public ResponseEntity<KeyVaultKeyModel> importKey(@PathVariable @Valid @Pattern(regexp = NAME_PATTERN) final String keyName,
@RequestAttribute(name = ApiConstants.REQUEST_BASE_URI) final URI baseUri,
@Valid @RequestBody final ImportKeyRequest request) {
public ResponseEntity<KeyVaultKeyModel> importKey(
@PathVariable @Valid @Pattern(regexp = NAME_PATTERN) final String keyName,
@RequestAttribute(name = ApiConstants.REQUEST_BASE_URI) final URI baseUri,
@Valid @RequestBody final ImportKeyRequest request) {
return super.importKey(keyName, baseUri, request);
}

@Override
@DeleteMapping(value = "/keys/{keyName}",
params = API_VERSION_7_2,
produces = APPLICATION_JSON_VALUE)
public ResponseEntity<KeyVaultKeyModel> delete(@PathVariable @Valid @Pattern(regexp = NAME_PATTERN) final String keyName,
@RequestAttribute(name = ApiConstants.REQUEST_BASE_URI) final URI baseUri) {
public ResponseEntity<KeyVaultKeyModel> delete(
@PathVariable @Valid @Pattern(regexp = NAME_PATTERN) final String keyName,
@RequestAttribute(name = ApiConstants.REQUEST_BASE_URI) final URI baseUri) {
return super.delete(keyName, baseUri);
}

Expand Down Expand Up @@ -101,7 +106,7 @@ public ResponseEntity<KeyVaultItemListModel<KeyVaultKeyItemModel>> listKeys(
@GetMapping(value = "/deletedkeys",
params = API_VERSION_7_2,
produces = APPLICATION_JSON_VALUE)
public ResponseEntity<KeyVaultItemListModel<KeyVaultKeyItemModel>> listDeletedKeys(
public ResponseEntity<KeyVaultItemListModel<DeletedKeyVaultKeyItemModel>> listDeletedKeys(
@RequestAttribute(name = ApiConstants.REQUEST_BASE_URI) final URI baseUri,
@RequestParam(name = MAX_RESULTS_PARAM, required = false, defaultValue = DEFAULT_MAX) final int maxResults,
@RequestParam(name = SKIP_TOKEN_PARAM, required = false, defaultValue = SKIP_ZERO) final int skipToken) {
Expand Down Expand Up @@ -146,26 +151,29 @@ public ResponseEntity<KeyVaultKeyModel> updateVersion(
@GetMapping(value = "/deletedkeys/{keyName}",
params = API_VERSION_7_2,
produces = APPLICATION_JSON_VALUE)
public ResponseEntity<KeyVaultKeyModel> getDeletedKey(@PathVariable @Valid @Pattern(regexp = NAME_PATTERN) final String keyName,
@RequestAttribute(name = ApiConstants.REQUEST_BASE_URI) final URI baseUri) {
public ResponseEntity<DeletedKeyVaultKeyModel> getDeletedKey(
@PathVariable @Valid @Pattern(regexp = NAME_PATTERN) final String keyName,
@RequestAttribute(name = ApiConstants.REQUEST_BASE_URI) final URI baseUri) {
return super.getDeletedKey(keyName, baseUri);
}

@Override
@PostMapping(value = "/deletedkeys/{keyName}/recover",
params = API_VERSION_7_2,
produces = APPLICATION_JSON_VALUE)
public ResponseEntity<KeyVaultKeyModel> recoverDeletedKey(@PathVariable @Valid @Pattern(regexp = NAME_PATTERN) final String keyName,
@RequestAttribute(name = ApiConstants.REQUEST_BASE_URI) final URI baseUri) {
public ResponseEntity<KeyVaultKeyModel> recoverDeletedKey(
@PathVariable @Valid @Pattern(regexp = NAME_PATTERN) final String keyName,
@RequestAttribute(name = ApiConstants.REQUEST_BASE_URI) final URI baseUri) {
return super.recoverDeletedKey(keyName, baseUri);
}

@Override
@DeleteMapping(value = "/deletedkeys/{keyName}",
params = API_VERSION_7_2,
produces = APPLICATION_JSON_VALUE)
public ResponseEntity<Void> purgeDeleted(@PathVariable @Valid @Pattern(regexp = NAME_PATTERN) final String keyName,
@RequestAttribute(name = ApiConstants.REQUEST_BASE_URI) final URI baseUri) {
public ResponseEntity<Void> purgeDeleted(
@PathVariable @Valid @Pattern(regexp = NAME_PATTERN) final String keyName,
@RequestAttribute(name = ApiConstants.REQUEST_BASE_URI) final URI baseUri) {
return super.purgeDeleted(keyName, baseUri);
}

Expand Down
Loading

0 comments on commit a1bc43b

Please sign in to comment.