Skip to content

Commit

Permalink
Backup and restore Keys/Secrets (#94)
Browse files Browse the repository at this point in the history
- Adds new controllers for backup and restore
- Defines backup and restore models
- Adds Base64 + Gzip serialization/deserialization for backups
- Adds new service methods to allow setting additional properties on entities
- Adds new test cases
- Updates readme

Resolves #86
{major}
  • Loading branch information
nagyesta authored Mar 26, 2022
1 parent 2dd35d5 commit 872a0d5
Show file tree
Hide file tree
Showing 62 changed files with 2,381 additions and 87 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ Lowkey Vault is far from supporting all Azure Key Vault features. The list suppo
- ```ES256K```
- ```ES384```
- ```ES512```
- Backup and restore keys

### Secrets

Expand All @@ -133,6 +134,7 @@ Lowkey Vault is far from supporting all Azure Key Vault features. The list suppo
- Update secret
- Recover deleted secret
- Purge deleted secret
- Backup and restore secrets

### Management API

Expand Down Expand Up @@ -196,4 +198,4 @@ This issue should not happen while using Testcontainers. See examples under [Low
- Some encryption/signature algorithms are not supported. Please refer to the ["Features"](#features) section for the up-to-date list of supported algorithms.
- Backup and restore features are not supported at the moment
- Certificate Vault features are not supported at the moment
- Recovery options cannot be set as vault creation is implicit during start-up
- Recovery options cannot be for vaults created during start-up
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package com.github.nagyesta.lowkeyvault.controller.v7_2;

import com.github.nagyesta.lowkeyvault.mapper.common.BackupConverter;
import com.github.nagyesta.lowkeyvault.mapper.common.RecoveryAwareConverter;
import com.github.nagyesta.lowkeyvault.model.v7_2.BasePropertiesModel;
import com.github.nagyesta.lowkeyvault.model.v7_2.common.BaseBackupListItem;
import com.github.nagyesta.lowkeyvault.model.v7_2.common.BaseBackupModel;
import com.github.nagyesta.lowkeyvault.service.EntityId;
import com.github.nagyesta.lowkeyvault.service.common.BaseVaultEntity;
import com.github.nagyesta.lowkeyvault.service.common.BaseVaultFake;
import com.github.nagyesta.lowkeyvault.service.common.ReadOnlyVersionedEntityMultiMap;
import com.github.nagyesta.lowkeyvault.service.common.impl.KeyVaultBaseEntity;
import com.github.nagyesta.lowkeyvault.service.vault.VaultFake;
import com.github.nagyesta.lowkeyvault.service.vault.VaultService;
import lombok.NonNull;
import org.springframework.util.Assert;

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

/**
* The base implementation of the backup/restore controllers.
*
* @param <K> The type of the entity id (not versioned).
* @param <V> The versioned entity id type.
* @param <E> The entity type.
* @param <M> The entity model type.
* @param <DM> The deleted entity model type.
* @param <P> The type of the properties model.
* @param <BLI> The type of the list item representing one entity version in the backup model.
* @param <BL> The wrapper type of the list in the backup model.
* @param <B> The type of the backup model.
* @param <MC> The model converter, converting entities to entity models.
* @param <BC> The converter, converting entities to list items of the backup models.
* @param <S> The fake type holding the entities.
*/
public abstract class BaseBackupRestoreController<K extends EntityId, V extends K, E extends BaseVaultEntity<V>, M, DM extends M,
P extends BasePropertiesModel, BLI extends BaseBackupListItem<P>, BL extends List<BLI>, B extends BaseBackupModel<P, BLI, BL>,
BC extends BackupConverter<V, E, P, BLI>, MC extends RecoveryAwareConverter<E, M, DM>,
S extends BaseVaultFake<K, V, E>> extends BaseEntityReadController<K, V, E, S> {

private final MC modelConverter;
private final BC backupConverter;

protected BaseBackupRestoreController(@NonNull final MC modelConverter,
@NonNull final BC backupConverter,
@org.springframework.lang.NonNull final VaultService vaultService,
@org.springframework.lang.NonNull final Function<VaultFake, S> toEntityVault) {
super(vaultService, toEntityVault);
this.modelConverter = modelConverter;
this.backupConverter = backupConverter;
}

protected M restoreEntity(final B backupModel) {
final URI baseUri = getSingleBaseUri(backupModel);
final S vault = getVaultByUri(baseUri);
final String id = getSingleEntityName(backupModel);
final K entityId = entityId(baseUri, id);
assertNameDoesNotExistYet(vault, entityId);
backupModel.getValue().forEach(entityVersion -> {
final V versionedEntityId = versionedEntityId(baseUri, id, entityVersion.getVersion());
restoreVersion(vault, versionedEntityId, entityVersion);
});
final V latestVersionOfEntity = vault.getEntities().getLatestVersionOfEntity(entityId);
final E readOnlyEntity = vault.getEntities().getReadOnlyEntity(latestVersionOfEntity);
return modelConverter.convert(readOnlyEntity);
}

protected abstract void restoreVersion(S vault, V versionedEntityId, BLI entityVersion);

protected void updateCommonFields(final BLI entityVersion, final KeyVaultBaseEntity<V> entity) {
final P attributes = entityVersion.getAttributes();
entity.setTags(Objects.requireNonNullElse(entityVersion.getTags(), Map.of()));
entity.setExpiry(attributes.getExpiresOn());
entity.setEnabled(attributes.isEnabled());
entity.setNotBefore(attributes.getNotBefore());
entity.setManaged(entityVersion.isManaged());
entity.setCreatedOn(attributes.getCreatedOn());
entity.setUpdatedOn(attributes.getUpdatedOn());
}

protected B backupEntity(final K entityId) {
final ReadOnlyVersionedEntityMultiMap<K, V, E> entities = getVaultByUri(entityId.vault())
.getEntities();
final List<BLI> list = entities.getVersions(entityId).stream()
.map(version -> getEntityByNameAndVersion(entityId.vault(), entityId.id(), version))
.map(backupConverter::convert)
.collect(Collectors.toUnmodifiableList());
return wrapBackup(list);
}

protected abstract B getBackupModel();

protected abstract BL getBackupList();

private B wrapBackup(final List<BLI> list) {
final BL listModel = Optional.ofNullable(list)
.map(l -> {
final BL backupList = getBackupList();
backupList.addAll(l);
return backupList;
})
.orElse(null);
final B backupModel = getBackupModel();
backupModel.setValue(listModel);
return backupModel;
}

private void assertNameDoesNotExistYet(final S vault, final K entityId) {
Assert.isTrue(!vault.getEntities().containsName(entityId.id()),
"Vault already contains entity with name: " + entityId.id());
Assert.isTrue(!vault.getDeletedEntities().containsName(entityId.id()),
"Vault already contains deleted entity with name: " + entityId.id());
}

private String getSingleEntityName(final B backupModel) {
final List<String> entityNames = backupModel.getValue().stream()
.map(BLI::getId)
.distinct()
.collect(Collectors.toUnmodifiableList());
Assert.isTrue(entityNames.size() == 1, "All backup entities must belong to the same entity.");
return entityNames.get(0);
}

private URI getSingleBaseUri(final B backupModel) {
final List<URI> uris = backupModel.getValue().stream()
.map(BLI::getVaultBaseUri)
.distinct()
.collect(Collectors.toUnmodifiableList());
Assert.isTrue(uris.size() == 1, "All backup entities must be from the same vault.");
return uris.get(0);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.github.nagyesta.lowkeyvault.controller.v7_2;

import com.github.nagyesta.lowkeyvault.controller.ErrorHandlingAwareController;
import com.github.nagyesta.lowkeyvault.service.EntityId;
import com.github.nagyesta.lowkeyvault.service.common.BaseVaultEntity;
import com.github.nagyesta.lowkeyvault.service.common.BaseVaultFake;
import com.github.nagyesta.lowkeyvault.service.exception.NotFoundException;
import com.github.nagyesta.lowkeyvault.service.vault.VaultFake;
import com.github.nagyesta.lowkeyvault.service.vault.VaultService;
import lombok.NonNull;

import java.net.URI;
import java.util.Optional;
import java.util.function.Function;

import static com.github.nagyesta.lowkeyvault.model.common.ApiConstants.V_7_2;

/**
* The base implementation of the backup/restore controllers.
*
* @param <K> The type of the entity id (not versioned).
* @param <V> The versioned entity id type.
* @param <E> The entity type.
* @param <S> The fake type holding the entities.
*/
public abstract class BaseEntityReadController<K extends EntityId, V extends K, E extends BaseVaultEntity<V>,
S extends BaseVaultFake<K, V, E>> extends ErrorHandlingAwareController {
/**
* API version.
*/
protected static final String API_VERSION_7_2 = "api-version=" + V_7_2;
/**
* RegExp of entity names (key name, secret name, certificate name).
*/
protected static final String NAME_PATTERN = "^[0-9a-zA-Z-]+$";
/**
* RegExp of entity version identifiers (key version, secret version, certificate version).
*/
protected static final String VERSION_NAME_PATTERN = "^[0-9a-f]{32}$";
private final VaultService vaultService;
private final Function<VaultFake, S> toEntityVault;

protected BaseEntityReadController(@NonNull final VaultService vaultService,
@org.springframework.lang.NonNull final Function<VaultFake, S> toEntityVault) {
this.vaultService = vaultService;
this.toEntityVault = toEntityVault;
}

protected E getEntityByNameAndVersion(final URI baseUri, final String name, final String version) {
final S vaultFake = getVaultByUri(baseUri);
final V entityId = versionedEntityId(baseUri, name, version);
return vaultFake.getEntities().getReadOnlyEntity(entityId);
}

protected S getVaultByUri(final URI baseUri) {
return Optional.of(vaultService.findByUri(baseUri))
.map(toEntityVault)
.orElseThrow(() -> new NotFoundException("Vault not found by base URI: " + baseUri));
}

protected abstract V versionedEntityId(URI baseUri, String name, String version);

protected abstract K entityId(URI baseUri, String name);

}
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package com.github.nagyesta.lowkeyvault.controller.v7_2;

import com.github.nagyesta.lowkeyvault.controller.ErrorHandlingAwareController;
import com.github.nagyesta.lowkeyvault.mapper.common.RecoveryAwareConverter;
import com.github.nagyesta.lowkeyvault.model.common.KeyVaultItemListModel;
import com.github.nagyesta.lowkeyvault.model.v7_2.BasePropertiesUpdateModel;
import com.github.nagyesta.lowkeyvault.service.EntityId;
import com.github.nagyesta.lowkeyvault.service.common.BaseVaultEntity;
import com.github.nagyesta.lowkeyvault.service.common.BaseVaultFake;
import com.github.nagyesta.lowkeyvault.service.exception.NotFoundException;
import com.github.nagyesta.lowkeyvault.service.vault.VaultFake;
import com.github.nagyesta.lowkeyvault.service.vault.VaultService;
import lombok.NonNull;
Expand All @@ -17,8 +15,6 @@
import java.util.function.Function;
import java.util.stream.Collectors;

import static com.github.nagyesta.lowkeyvault.model.common.ApiConstants.V_7_2;

/**
* The base implementation of the entity controllers.
*
Expand All @@ -34,22 +30,10 @@
* @param <VIC> The versioned item converter, converting version item entities to item models.
* @param <S> The fake type holding the entities.
*/
public abstract class BaseController<K extends EntityId, V extends K, E extends BaseVaultEntity<V>,
public abstract class GenericEntityController<K extends EntityId, V extends K, E extends BaseVaultEntity<V>,
M, DM extends M, I, DI extends I, MC extends RecoveryAwareConverter<E, M, DM>,
IC extends RecoveryAwareConverter<E, I, DI>, VIC extends RecoveryAwareConverter<E, I, DI>,
S extends BaseVaultFake<K, V, E>> extends ErrorHandlingAwareController {
/**
* API version.
*/
protected static final String API_VERSION_7_2 = "api-version=" + V_7_2;
/**
* RegExp of entity names (key name, secret name, certificate name).
*/
protected static final String NAME_PATTERN = "^[0-9a-zA-Z-]+$";
/**
* RegExp of entity version identifiers (key version, secret version, certificate version).
*/
protected static final String VERSION_NAME_PATTERN = "^[0-9a-f]{32}$";
S extends BaseVaultFake<K, V, E>> extends BaseEntityReadController<K, V, E, S> {
/**
* Default page size used when returning available versions of an entity.
*/
Expand All @@ -69,16 +53,16 @@ public abstract class BaseController<K extends EntityId, V extends K, E extends
private final MC modelConverter;
private final IC itemConverter;
private final VIC versionedItemConverter;
private final VaultService vaultService;
private final Function<VaultFake, S> toEntityVault;

protected BaseController(@NonNull final MC modelConverter, @NonNull final IC itemConverter, @NonNull final VIC versionedItemConverter,
@NonNull final VaultService vaultService, final Function<VaultFake, S> toEntityVault) {
protected GenericEntityController(@NonNull final MC modelConverter,
@NonNull final IC itemConverter,
@NonNull final VIC versionedItemConverter,
@org.springframework.lang.NonNull final VaultService vaultService,
@org.springframework.lang.NonNull final Function<VaultFake, S> toEntityVault) {
super(vaultService, toEntityVault);
this.modelConverter = modelConverter;
this.itemConverter = itemConverter;
this.versionedItemConverter = versionedItemConverter;
this.vaultService = vaultService;
this.toEntityVault = toEntityVault;
}

protected M getModelById(final S entityVaultFake, final V entityId) {
Expand Down Expand Up @@ -126,24 +110,12 @@ protected KeyVaultItemListModel<I> getPageOfDeletedItems(final URI baseUri, fina
return listModel(items, nextUri);
}

protected E getEntityByNameAndVersion(final URI baseUri, final String name, final String version) {
final S vaultFake = getVaultByUri(baseUri);
final V entityId = versionedEntityId(baseUri, name, version);
return vaultFake.getEntities().getReadOnlyEntity(entityId);
}

protected M getLatestEntityModel(final URI baseUri, final String name) {
final S vaultFake = getVaultByUri(baseUri);
final V entityId = vaultFake.getEntities().getLatestVersionOfEntity(entityId(baseUri, name));
return getModelById(vaultFake, entityId);
}

protected S getVaultByUri(final URI baseUri) {
return Optional.ofNullable(vaultService.findByUri(baseUri))
.map(toEntityVault)
.orElseThrow(() -> new NotFoundException("Vault not found by base URI: " + baseUri));
}

protected void updateAttributes(final BaseVaultFake<K, V, ?> vaultFake, final V entityId, final BasePropertiesUpdateModel properties) {
Optional.ofNullable(properties)
.ifPresent(attributes -> {
Expand All @@ -167,10 +139,6 @@ protected KeyVaultItemListModel<I> listModel(final List<I> items, final URI next
return new KeyVaultItemListModel<>(items, nextUri);
}

protected abstract V versionedEntityId(URI baseUri, String name, String version);

protected abstract K entityId(URI baseUri, String name);

private URI getNextUri(final String prefix, final Collection<?> allItems,
final Collection<?> items, final int limit, final int offset) {
URI nextUri = null;
Expand Down
Loading

0 comments on commit 872a0d5

Please sign in to comment.