From 872a0d5ea6141a047a65b37622328f2e0c45c01a Mon Sep 17 00:00:00 2001 From: Esta Nagy Date: Sat, 26 Mar 2022 13:16:39 +0100 Subject: [PATCH] Backup and restore Keys/Secrets (#94) - 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} --- README.md | 4 +- .../v7_2/BaseBackupRestoreController.java | 139 +++++++++ .../v7_2/BaseEntityReadController.java | 65 +++++ ...ller.java => GenericEntityController.java} | 48 +-- .../v7_2/KeyBackupRestoreController.java | 96 ++++++ .../controller/v7_2/KeyController.java | 4 +- .../controller/v7_2/KeyCryptoController.java | 2 +- .../v7_2/SecretBackupRestoreController.java | 92 ++++++ .../controller/v7_2/SecretController.java | 3 +- .../mapper/common/BackupConverter.java | 40 +++ .../key/KeyEntityToV72BackupConverter.java | 79 +++++ .../key/KeyEntityToV72ModelConverter.java | 1 + .../RsaJsonWebKeyImportRequestConverter.java | 7 +- .../util/AbstractBase64ZipDeserializer.java | 52 ++++ .../util/AbstractBase64ZipSerializer.java | 59 ++++ .../model/json/util/Base64Deserializer.java | 4 + .../model/json/util/Base64Serializer.java | 16 +- .../json/util/Base64ZipKeyDeserializer.java | 20 ++ .../json/util/Base64ZipKeySerializer.java | 15 + .../util/Base64ZipSecretDeserializer.java | 21 ++ .../json/util/Base64ZipSecretSerializer.java | 15 + .../model/v7_2/common/BaseBackupListItem.java | 41 +++ .../model/v7_2/common/BaseBackupModel.java | 27 ++ .../model/v7_2/key/KeyBackupList.java | 6 + .../model/v7_2/key/KeyBackupListItem.java | 21 ++ .../model/v7_2/key/KeyBackupModel.java | 28 ++ .../model/v7_2/key/constants/KeyType.java | 24 +- .../v7_2/secret/KeyVaultSecretModel.java | 3 - .../model/v7_2/secret/SecretBackupList.java | 6 + .../v7_2/secret/SecretBackupListItem.java | 21 ++ .../model/v7_2/secret/SecretBackupModel.java | 28 ++ .../SecretEntityToV72BackupConverter.java | 28 ++ .../service/common/BaseVaultEntity.java | 6 + .../common/impl/KeyVaultBaseEntity.java | 10 + .../impl/KeyVaultLifecycleAwareEntity.java | 9 + .../lowkeyvault/service/key/KeyVaultFake.java | 2 + .../key/ReadOnlyEcKeyVaultKeyEntity.java | 2 + .../key/ReadOnlyRsaKeyVaultKeyEntity.java | 12 + .../key/impl/AesKeyVaultKeyEntity.java | 3 +- .../service/key/impl/EcKeyVaultKeyEntity.java | 12 +- .../service/key/impl/KeyVaultFakeImpl.java | 5 + .../key/impl/RsaKeyVaultKeyEntity.java | 31 ++ .../service/secret/SecretVaultFake.java | 2 + .../secret/impl/SecretVaultFakeImpl.java | 6 + ...ackupRestoreControllerIntegrationTest.java | 274 ++++++++++++++++++ ...ackupRestoreControllerIntegrationTest.java | 247 ++++++++++++++++ .../key/KeyEntityToV72ModelConverterTest.java | 2 +- .../model/json/util/Base64SerializerTest.java | 2 +- .../util/Base64ZipKeyDeserializerTest.java | 49 ++++ ...SerializerDeserializerIntegrationTest.java | 121 ++++++++ .../json/util/Base64ZipKeySerializerTest.java | 48 +++ ...SerializerDeserializerIntegrationTest.java | 105 +++++++ .../KeyEntityToV72BackupConverterTest.java | 196 +++++++++++++ .../SecretEntityToV72BackupConverterTest.java | 108 +++++++ .../secret/impl/SecretVaultFakeImplTest.java | 32 +- .../context/CommonTestContext.java | 14 +- .../hook/MissionOutlineDefinition.java | 19 +- .../lowkeyvault/steps/KeysStepDefs.java | 13 + .../steps/KeysStepDefsAssertions.java | 6 + .../lowkeyvault/steps/SecretsStepDefs.java | 15 +- .../keys/BackupAndRestoreKeys.feature | 79 +++++ .../secrets/BackupAndRestoreSecrets.feature | 23 ++ 62 files changed, 2381 insertions(+), 87 deletions(-) create mode 100644 lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/BaseBackupRestoreController.java create mode 100644 lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/BaseEntityReadController.java rename lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/{BaseController.java => GenericEntityController.java} (78%) create mode 100644 lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/KeyBackupRestoreController.java create mode 100644 lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/SecretBackupRestoreController.java create mode 100644 lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/mapper/common/BackupConverter.java create mode 100644 lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/mapper/v7_2/key/KeyEntityToV72BackupConverter.java create mode 100644 lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/json/util/AbstractBase64ZipDeserializer.java create mode 100644 lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/json/util/AbstractBase64ZipSerializer.java create mode 100644 lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64ZipKeyDeserializer.java create mode 100644 lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64ZipKeySerializer.java create mode 100644 lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64ZipSecretDeserializer.java create mode 100644 lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64ZipSecretSerializer.java create mode 100644 lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/common/BaseBackupListItem.java create mode 100644 lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/common/BaseBackupModel.java create mode 100644 lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/KeyBackupList.java create mode 100644 lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/KeyBackupListItem.java create mode 100644 lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/KeyBackupModel.java create mode 100644 lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/secret/SecretBackupList.java create mode 100644 lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/secret/SecretBackupListItem.java create mode 100644 lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/secret/SecretBackupModel.java create mode 100644 lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/secret/SecretEntityToV72BackupConverter.java create mode 100644 lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/controller/v7_2/KeyBackupRestoreControllerIntegrationTest.java create mode 100644 lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/controller/v7_2/SecretBackupRestoreControllerIntegrationTest.java create mode 100644 lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64ZipKeyDeserializerTest.java create mode 100644 lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64ZipKeySerializerDeserializerIntegrationTest.java create mode 100644 lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64ZipKeySerializerTest.java create mode 100644 lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64ZipSecretSerializerDeserializerIntegrationTest.java create mode 100644 lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/KeyEntityToV72BackupConverterTest.java create mode 100644 lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/model/v7_2/secret/SecretEntityToV72BackupConverterTest.java create mode 100644 lowkey-vault-docker/src/test/resources/com/github/nagyesta/lowkeyvault/keys/BackupAndRestoreKeys.feature create mode 100644 lowkey-vault-docker/src/test/resources/com/github/nagyesta/lowkeyvault/secrets/BackupAndRestoreSecrets.feature diff --git a/README.md b/README.md index f8c6b81a..b9e67cd6 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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 diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/BaseBackupRestoreController.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/BaseBackupRestoreController.java new file mode 100644 index 00000000..33bd41b4 --- /dev/null +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/BaseBackupRestoreController.java @@ -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 The type of the entity id (not versioned). + * @param The versioned entity id type. + * @param The entity type. + * @param The entity model type. + * @param The deleted entity model type. + * @param

The type of the properties model. + * @param The type of the list item representing one entity version in the backup model. + * @param The wrapper type of the list in the backup model. + * @param The type of the backup model. + * @param The model converter, converting entities to entity models. + * @param The converter, converting entities to list items of the backup models. + * @param The fake type holding the entities. + */ +public abstract class BaseBackupRestoreController, M, DM extends M, + P extends BasePropertiesModel, BLI extends BaseBackupListItem

, BL extends List, B extends BaseBackupModel, + BC extends BackupConverter, MC extends RecoveryAwareConverter, + S extends BaseVaultFake> extends BaseEntityReadController { + + 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 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 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 entities = getVaultByUri(entityId.vault()) + .getEntities(); + final List 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 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 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 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); + } + +} diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/BaseEntityReadController.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/BaseEntityReadController.java new file mode 100644 index 00000000..f375e8a4 --- /dev/null +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/BaseEntityReadController.java @@ -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 The type of the entity id (not versioned). + * @param The versioned entity id type. + * @param The entity type. + * @param The fake type holding the entities. + */ +public abstract class BaseEntityReadController, + S extends BaseVaultFake> 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 toEntityVault; + + protected BaseEntityReadController(@NonNull final VaultService vaultService, + @org.springframework.lang.NonNull final Function 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); + +} diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/BaseController.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/GenericEntityController.java similarity index 78% rename from lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/BaseController.java rename to lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/GenericEntityController.java index 9d3951a0..fe439bb5 100644 --- a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/BaseController.java +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/GenericEntityController.java @@ -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; @@ -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. * @@ -34,22 +30,10 @@ * @param The versioned item converter, converting version item entities to item models. * @param The fake type holding the entities. */ -public abstract class BaseController, +public abstract class GenericEntityController, M, DM extends M, I, DI extends I, MC extends RecoveryAwareConverter, IC extends RecoveryAwareConverter, VIC extends RecoveryAwareConverter, - S extends BaseVaultFake> 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> extends BaseEntityReadController { /** * Default page size used when returning available versions of an entity. */ @@ -69,16 +53,16 @@ public abstract class BaseController toEntityVault; - protected BaseController(@NonNull final MC modelConverter, @NonNull final IC itemConverter, @NonNull final VIC versionedItemConverter, - @NonNull final VaultService vaultService, final Function 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 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) { @@ -126,24 +110,12 @@ protected KeyVaultItemListModel 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 vaultFake, final V entityId, final BasePropertiesUpdateModel properties) { Optional.ofNullable(properties) .ifPresent(attributes -> { @@ -167,10 +139,6 @@ protected KeyVaultItemListModel listModel(final List 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; diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/KeyBackupRestoreController.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/KeyBackupRestoreController.java new file mode 100644 index 00000000..bdaf35ef --- /dev/null +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/KeyBackupRestoreController.java @@ -0,0 +1,96 @@ +package com.github.nagyesta.lowkeyvault.controller.v7_2; + +import com.github.nagyesta.lowkeyvault.mapper.v7_2.key.KeyEntityToV72BackupConverter; +import com.github.nagyesta.lowkeyvault.mapper.v7_2.key.KeyEntityToV72ModelConverter; +import com.github.nagyesta.lowkeyvault.model.common.ApiConstants; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.*; +import com.github.nagyesta.lowkeyvault.service.key.KeyVaultFake; +import com.github.nagyesta.lowkeyvault.service.key.ReadOnlyKeyVaultKeyEntity; +import com.github.nagyesta.lowkeyvault.service.key.id.KeyEntityId; +import com.github.nagyesta.lowkeyvault.service.key.id.VersionedKeyEntityId; +import com.github.nagyesta.lowkeyvault.service.key.impl.KeyVaultKeyEntity; +import com.github.nagyesta.lowkeyvault.service.vault.VaultFake; +import com.github.nagyesta.lowkeyvault.service.vault.VaultService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.lang.NonNull; +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 java.util.Collections; +import java.util.Objects; + +import static com.github.nagyesta.lowkeyvault.model.common.ApiConstants.V_7_2; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +@Slf4j +@RestController +@Validated +public class KeyBackupRestoreController extends BaseBackupRestoreController { + + @Autowired + protected KeyBackupRestoreController( + @NonNull final KeyEntityToV72ModelConverter modelConverter, + @NonNull final KeyEntityToV72BackupConverter backupConverter, + @NonNull final VaultService vaultService) { + super(modelConverter, backupConverter, vaultService, VaultFake::keyVaultFake); + } + + @PostMapping(value = "/keys/{keyName}/backup", + params = API_VERSION_7_2, + consumes = APPLICATION_JSON_VALUE, + produces = APPLICATION_JSON_VALUE) + public ResponseEntity backup(@PathVariable @Valid @Pattern(regexp = NAME_PATTERN) final String keyName, + @RequestAttribute(name = ApiConstants.REQUEST_BASE_URI) final URI baseUri) { + log.info("Received request to {} backup key: {} using API version: {}", + baseUri.toString(), keyName, V_7_2); + return ResponseEntity.ok(backupEntity(entityId(baseUri, keyName))); + } + + @PostMapping(value = "/keys/restore", + params = API_VERSION_7_2, + consumes = APPLICATION_JSON_VALUE, + produces = APPLICATION_JSON_VALUE) + public ResponseEntity restore(@RequestAttribute(name = ApiConstants.REQUEST_BASE_URI) final URI baseUri, + @Valid @RequestBody final KeyBackupModel keyBackupModel) { + log.info("Received request to {} restore key: {} using API version: {}", + baseUri.toString(), keyBackupModel.getValue().get(0).getId(), V_7_2); + return ResponseEntity.ok(restoreEntity(keyBackupModel)); + } + + @Override + protected void restoreVersion(@NonNull final KeyVaultFake vault, + @NonNull final VersionedKeyEntityId versionedEntityId, + @NonNull final KeyBackupListItem entityVersion) { + vault.importKeyVersion(versionedEntityId, entityVersion.getKeyMaterial()); + final KeyVaultKeyEntity entity = vault.getEntities().getEntity(versionedEntityId, KeyVaultKeyEntity.class); + entity.setOperations(Objects.requireNonNullElse(entityVersion.getKeyMaterial().getKeyOps(), Collections.emptyList())); + updateCommonFields(entityVersion, entity); + } + + @Override + protected KeyBackupList getBackupList() { + return new KeyBackupList(); + } + + @Override + protected KeyBackupModel getBackupModel() { + return new KeyBackupModel(); + } + + @Override + protected VersionedKeyEntityId versionedEntityId(final URI baseUri, final String name, final String version) { + return new VersionedKeyEntityId(baseUri, name, version); + } + + @Override + protected KeyEntityId entityId(final URI baseUri, final String name) { + return new KeyEntityId(baseUri, name); + } +} diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/KeyController.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/KeyController.java index 7681d362..714051da 100644 --- a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/KeyController.java +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/KeyController.java @@ -37,7 +37,7 @@ @Slf4j @RestController @Validated -public class KeyController extends BaseController { @@ -253,6 +253,7 @@ private VersionedKeyEntityId createKeyWithAttributes( keyVaultFake.addTags(keyEntityId, request.getTags()); keyVaultFake.setExpiry(keyEntityId, properties.getNotBefore(), properties.getExpiresOn()); keyVaultFake.setEnabled(keyEntityId, properties.isEnabled()); + //no need to set managed property as this endpoint cannot create managed entities by definition return keyEntityId; } @@ -267,6 +268,7 @@ private VersionedKeyEntityId importKeyWithAttributes( keyVaultFake.addTags(keyEntityId, request.getTags()); keyVaultFake.setExpiry(keyEntityId, properties.getNotBefore(), properties.getExpiresOn()); keyVaultFake.setEnabled(keyEntityId, Objects.requireNonNullElse(properties.getEnabled(), true)); + //no need to set managed property as this endpoint cannot create managed entities by definition return keyEntityId; } } diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/KeyCryptoController.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/KeyCryptoController.java index 8530ef5c..3f4f365e 100644 --- a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/KeyCryptoController.java +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/KeyCryptoController.java @@ -31,7 +31,7 @@ @Slf4j @RestController @Validated -public class KeyCryptoController extends BaseController { diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/SecretBackupRestoreController.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/SecretBackupRestoreController.java new file mode 100644 index 00000000..47fa0cd9 --- /dev/null +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/SecretBackupRestoreController.java @@ -0,0 +1,92 @@ +package com.github.nagyesta.lowkeyvault.controller.v7_2; + +import com.github.nagyesta.lowkeyvault.mapper.v7_2.secret.SecretEntityToV72ModelConverter; +import com.github.nagyesta.lowkeyvault.model.common.ApiConstants; +import com.github.nagyesta.lowkeyvault.model.v7_2.secret.*; +import com.github.nagyesta.lowkeyvault.service.secret.ReadOnlyKeyVaultSecretEntity; +import com.github.nagyesta.lowkeyvault.service.secret.SecretVaultFake; +import com.github.nagyesta.lowkeyvault.service.secret.id.SecretEntityId; +import com.github.nagyesta.lowkeyvault.service.secret.id.VersionedSecretEntityId; +import com.github.nagyesta.lowkeyvault.service.secret.impl.KeyVaultSecretEntity; +import com.github.nagyesta.lowkeyvault.service.vault.VaultFake; +import com.github.nagyesta.lowkeyvault.service.vault.VaultService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.lang.NonNull; +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.V_7_2; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +@Slf4j +@RestController +@Validated +public class SecretBackupRestoreController extends BaseBackupRestoreController { + + @Autowired + protected SecretBackupRestoreController( + @NonNull final SecretEntityToV72ModelConverter modelConverter, + @NonNull final SecretEntityToV72BackupConverter backupConverter, + @NonNull final VaultService vaultService) { + super(modelConverter, backupConverter, vaultService, VaultFake::secretVaultFake); + } + + @PostMapping(value = "/secrets/{secretName}/backup", + params = API_VERSION_7_2, + consumes = APPLICATION_JSON_VALUE, + produces = APPLICATION_JSON_VALUE) + public ResponseEntity backup(@PathVariable @Valid @Pattern(regexp = NAME_PATTERN) final String secretName, + @RequestAttribute(name = ApiConstants.REQUEST_BASE_URI) final URI baseUri) { + log.info("Received request to {} backup secret: {} using API version: {}", + baseUri.toString(), secretName, V_7_2); + return ResponseEntity.ok(backupEntity(entityId(baseUri, secretName))); + } + + @PostMapping(value = "/secrets/restore", + params = API_VERSION_7_2, + consumes = APPLICATION_JSON_VALUE, + produces = APPLICATION_JSON_VALUE) + public ResponseEntity restore(@RequestAttribute(name = ApiConstants.REQUEST_BASE_URI) final URI baseUri, + @Valid @RequestBody final SecretBackupModel secretBackupModel) { + log.info("Received request to {} restore secret: {} using API version: {}", + baseUri.toString(), secretBackupModel.getValue().get(0).getId(), V_7_2); + return ResponseEntity.ok(restoreEntity(secretBackupModel)); + } + + @Override + protected void restoreVersion(@NonNull final SecretVaultFake vault, + @NonNull final VersionedSecretEntityId versionedEntityId, + @NonNull final SecretBackupListItem entityVersion) { + vault.createSecretVersion(versionedEntityId, entityVersion.getValue(), entityVersion.getContentType()); + final KeyVaultSecretEntity entity = vault.getEntities().getEntity(versionedEntityId, KeyVaultSecretEntity.class); + updateCommonFields(entityVersion, entity); + } + + @Override + protected SecretBackupList getBackupList() { + return new SecretBackupList(); + } + + @Override + protected SecretBackupModel getBackupModel() { + return new SecretBackupModel(); + } + + @Override + protected VersionedSecretEntityId versionedEntityId(final URI baseUri, final String name, final String version) { + return new VersionedSecretEntityId(baseUri, name, version); + } + + @Override + protected SecretEntityId entityId(final URI baseUri, final String name) { + return new SecretEntityId(baseUri, name); + } +} diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/SecretController.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/SecretController.java index 448d60ba..56951c97 100644 --- a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/SecretController.java +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/SecretController.java @@ -32,7 +32,7 @@ @Slf4j @RestController @Validated -public class SecretController extends BaseController { @@ -236,6 +236,7 @@ private VersionedSecretEntityId createSecretWithAttributes( secretVaultFake.addTags(secretEntityId, request.getTags()); secretVaultFake.setExpiry(secretEntityId, properties.getNotBefore(), properties.getExpiresOn()); secretVaultFake.setEnabled(secretEntityId, properties.isEnabled()); + //no need to set managed property as this endpoint cannot create managed entities by definition return secretEntityId; } } diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/mapper/common/BackupConverter.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/mapper/common/BackupConverter.java new file mode 100644 index 00000000..e5db0db7 --- /dev/null +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/mapper/common/BackupConverter.java @@ -0,0 +1,40 @@ +package com.github.nagyesta.lowkeyvault.mapper.common; + +import com.github.nagyesta.lowkeyvault.model.v7_2.BasePropertiesModel; +import com.github.nagyesta.lowkeyvault.model.v7_2.common.BaseBackupListItem; +import com.github.nagyesta.lowkeyvault.service.EntityId; +import com.github.nagyesta.lowkeyvault.service.common.BaseVaultEntity; +import org.springframework.core.convert.converter.Converter; +import org.springframework.lang.NonNull; + +import java.util.Map; + +public abstract class BackupConverter, P extends BasePropertiesModel, + BLI extends BaseBackupListItem

> implements Converter { + + private final Converter propertiesConverter; + + protected BackupConverter(@lombok.NonNull final Converter propertiesConverter) { + this.propertiesConverter = propertiesConverter; + } + + @NonNull + @Override + public BLI convert(@NonNull final E source) { + final BLI item = convertUniqueFields(source); + return mapCommonFields(source, item); + } + + protected abstract BLI convertUniqueFields(E source); + + private BLI mapCommonFields(final E source, final BLI item) { + final V entityId = source.getId(); + item.setVaultBaseUri(entityId.vault()); + item.setId(entityId.id()); + item.setVersion(entityId.version()); + item.setAttributes(propertiesConverter.convert(source)); + item.setTags(Map.copyOf(source.getTags())); + item.setManaged(source.isManaged()); + return item; + } +} diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/mapper/v7_2/key/KeyEntityToV72BackupConverter.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/mapper/v7_2/key/KeyEntityToV72BackupConverter.java new file mode 100644 index 00000000..c45b5c77 --- /dev/null +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/mapper/v7_2/key/KeyEntityToV72BackupConverter.java @@ -0,0 +1,79 @@ +package com.github.nagyesta.lowkeyvault.mapper.v7_2.key; + +import com.github.nagyesta.lowkeyvault.mapper.common.BackupConverter; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.KeyBackupListItem; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.KeyPropertiesModel; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.request.JsonWebKeyImportRequest; +import com.github.nagyesta.lowkeyvault.service.key.ReadOnlyAesKeyVaultKeyEntity; +import com.github.nagyesta.lowkeyvault.service.key.ReadOnlyEcKeyVaultKeyEntity; +import com.github.nagyesta.lowkeyvault.service.key.ReadOnlyKeyVaultKeyEntity; +import com.github.nagyesta.lowkeyvault.service.key.ReadOnlyRsaKeyVaultKeyEntity; +import com.github.nagyesta.lowkeyvault.service.key.id.VersionedKeyEntityId; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.convert.converter.Converter; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +@Component +public class KeyEntityToV72BackupConverter + extends BackupConverter { + + @Autowired + public KeyEntityToV72BackupConverter( + @NonNull final Converter propertiesConverter) { + super(propertiesConverter); + } + + @Override + protected KeyBackupListItem convertUniqueFields(@NonNull final ReadOnlyKeyVaultKeyEntity source) { + final JsonWebKeyImportRequest keyMaterial = convertKeyMaterial(source); + final KeyBackupListItem listItem = new KeyBackupListItem(); + listItem.setKeyMaterial(populateCommonKeyFields(source, keyMaterial)); + return listItem; + } + + private JsonWebKeyImportRequest convertKeyMaterial(final ReadOnlyKeyVaultKeyEntity source) { + final JsonWebKeyImportRequest keyMaterial = new JsonWebKeyImportRequest(); + if (source.getKeyType().isRsa()) { + convertRsaFields((ReadOnlyRsaKeyVaultKeyEntity) source, keyMaterial); + } else if (source.getKeyType().isEc()) { + convertEcFields((ReadOnlyEcKeyVaultKeyEntity) source, keyMaterial); + } else { + Assert.isTrue(source.getKeyType().isOct(), "Unknown key type found: " + source.getKeyType()); + convertOctFields((ReadOnlyAesKeyVaultKeyEntity) source, keyMaterial); + } + return keyMaterial; + } + + private void convertOctFields(final ReadOnlyAesKeyVaultKeyEntity source, final JsonWebKeyImportRequest keyMaterial) { + keyMaterial.setK(source.getK()); + } + + private void convertEcFields(final ReadOnlyEcKeyVaultKeyEntity source, final JsonWebKeyImportRequest keyMaterial) { + keyMaterial.setCurveName(source.getKeyCurveName()); + keyMaterial.setX(source.getX()); + keyMaterial.setY(source.getY()); + keyMaterial.setD(source.getD()); + } + + private void convertRsaFields(final ReadOnlyRsaKeyVaultKeyEntity source, final JsonWebKeyImportRequest keyMaterial) { + keyMaterial.setN(source.getN()); + keyMaterial.setE(source.getE()); + keyMaterial.setD(source.getD()); + keyMaterial.setDp(source.getDp()); + keyMaterial.setDq(source.getDq()); + keyMaterial.setP(source.getP()); + keyMaterial.setQ(source.getQ()); + keyMaterial.setQi(source.getQi()); + } + + private JsonWebKeyImportRequest populateCommonKeyFields( + final ReadOnlyKeyVaultKeyEntity source, final JsonWebKeyImportRequest keyMaterial) { + keyMaterial.setId(source.getId().asUri().toString()); + keyMaterial.setKeyType(source.getKeyType()); + keyMaterial.setKeyOps(source.getOperations()); + keyMaterial.setKeyHsm(null); + return keyMaterial; + } +} diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/mapper/v7_2/key/KeyEntityToV72ModelConverter.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/mapper/v7_2/key/KeyEntityToV72ModelConverter.java index e910d048..b8d81e55 100644 --- a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/mapper/v7_2/key/KeyEntityToV72ModelConverter.java +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/mapper/v7_2/key/KeyEntityToV72ModelConverter.java @@ -63,6 +63,7 @@ private JsonWebKeyModel mapEcFields(final ReadOnlyEcKeyVaultKeyEntity entity) { } private JsonWebKeyModel mapOctFields(final ReadOnlyAesKeyVaultKeyEntity entity) { + //Do not map K to avoid exposing key material return mapCommonKeyProperties(entity); } diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/mapper/v7_2/key/RsaJsonWebKeyImportRequestConverter.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/mapper/v7_2/key/RsaJsonWebKeyImportRequestConverter.java index ed960db1..7fe1a235 100644 --- a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/mapper/v7_2/key/RsaJsonWebKeyImportRequestConverter.java +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/mapper/v7_2/key/RsaJsonWebKeyImportRequestConverter.java @@ -1,5 +1,6 @@ package com.github.nagyesta.lowkeyvault.mapper.v7_2.key; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.KeyType; import com.github.nagyesta.lowkeyvault.model.v7_2.key.request.JsonWebKeyImportRequest; import com.github.nagyesta.lowkeyvault.service.exception.CryptoException; import org.springframework.lang.NonNull; @@ -11,6 +12,7 @@ import java.security.spec.RSAPrivateCrtKeySpec; import java.security.spec.RSAPrivateKeySpec; import java.security.spec.RSAPublicKeySpec; +import java.util.SortedSet; /** * Converts import requests to RSA key pairs. @@ -34,7 +36,10 @@ public KeyPair convert(@NonNull final JsonWebKeyImportRequest source) { @Override public Integer getKeyParameter(@NonNull final JsonWebKeyImportRequest source) { - return source.getN().length * RSA_MODULUS_BYTES_TO_KEY_SIZE_BITS_MULTIPLIER; + final int calculatedWithPotentialLeadingZero = source.getN().length * RSA_MODULUS_BYTES_TO_KEY_SIZE_BITS_MULTIPLIER; + final SortedSet validValuesBelowLimit = KeyType.RSA.getValidKeyParameters(Integer.class) + .headSet(calculatedWithPotentialLeadingZero + 1); + return validValuesBelowLimit.last(); } private RSAPublicKeySpec rsaPublicKeySpec(final JsonWebKeyImportRequest source) { diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/json/util/AbstractBase64ZipDeserializer.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/json/util/AbstractBase64ZipDeserializer.java new file mode 100644 index 00000000..0a5d87cb --- /dev/null +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/json/util/AbstractBase64ZipDeserializer.java @@ -0,0 +1,52 @@ +package com.github.nagyesta.lowkeyvault.model.json.util; + +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Optional; +import java.util.zip.GZIPInputStream; + +/** + * Deserializer Base64 decoding and unzipping json snippets. + * + * @param The type of the entity. + */ +@Slf4j +public abstract class AbstractBase64ZipDeserializer extends JsonDeserializer { + + private final Base64Deserializer base64Deserializer; + private final ObjectMapper objectMapper; + + protected AbstractBase64ZipDeserializer(final Base64Deserializer base64Deserializer, final ObjectMapper objectMapper) { + this.base64Deserializer = base64Deserializer; + this.objectMapper = objectMapper; + } + + @Override + public E deserialize(final JsonParser jsonParser, final DeserializationContext context) throws IOException, JacksonException { + final Optional bytes = Optional.ofNullable(base64Deserializer.deserializeBase64(jsonParser)); + return bytes.filter(v -> v.length > 0) + .map(this::decompressWrappedObject) + .orElse(null); + } + + private E decompressWrappedObject(final byte[] bytes) { + //noinspection LocalCanBeFinal + try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); + GZIPInputStream gzipInputStream = new GZIPInputStream(byteArrayInputStream)) { + final String json = new String(gzipInputStream.readAllBytes()); + return objectMapper.reader().readValue(json, getType()); + } catch (Exception e) { + log.error(e.getMessage(), e); + throw new IllegalArgumentException("Unable to decompress input."); + } + } + + protected abstract Class getType(); +} diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/json/util/AbstractBase64ZipSerializer.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/json/util/AbstractBase64ZipSerializer.java new file mode 100644 index 00000000..5f043995 --- /dev/null +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/json/util/AbstractBase64ZipSerializer.java @@ -0,0 +1,59 @@ +package com.github.nagyesta.lowkeyvault.model.json.util; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import lombok.extern.slf4j.Slf4j; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import java.util.zip.GZIPOutputStream; + +/** + * Serializer zipping json snippets and encoding with base64. + * + * @param The type of the entity. + */ +@Slf4j +public abstract class AbstractBase64ZipSerializer extends JsonSerializer { + + private final Base64Serializer base64Serializer; + private final ObjectMapper objectMapper; + + protected AbstractBase64ZipSerializer(final Base64Serializer base64Serializer, final ObjectMapper objectMapper) { + this.base64Serializer = base64Serializer; + this.objectMapper = objectMapper; + } + + @Override + public void serialize(final E value, final JsonGenerator gen, + final SerializerProvider serializers) throws IOException { + final String base64 = Optional.ofNullable(value) + .map(this::compressObject) + .orElse(null); + if (base64 != null) { + gen.writeString(base64); + } else { + gen.writeNull(); + } + } + + private String compressObject(final E value) { + //noinspection LocalCanBeFinal + try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + GZIPOutputStream gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream)) { + final String json = objectMapper.writer().writeValueAsString(value); + gzipOutputStream.write(json.getBytes(StandardCharsets.UTF_8)); + gzipOutputStream.flush(); + gzipOutputStream.finish(); + final byte[] byteArray = byteArrayOutputStream.toByteArray(); + return base64Serializer.base64Encode(byteArray); + } catch (Exception e) { + log.error(e.getMessage(), e); + throw new IllegalArgumentException("Unable to compress input."); + } + } +} diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64Deserializer.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64Deserializer.java index 56ad56c5..10a5c4e6 100644 --- a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64Deserializer.java +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64Deserializer.java @@ -14,6 +14,10 @@ public class Base64Deserializer extends JsonDeserializer { @Override public byte[] deserialize(final JsonParser parser, final DeserializationContext context) throws IOException { + return deserializeBase64(parser); + } + + protected byte[] deserializeBase64(final JsonParser parser) throws IOException { final Optional optional = Optional.ofNullable(parser.readValueAs(String.class)); if (optional.isEmpty()) { return null; diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64Serializer.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64Serializer.java index 8943560c..4f444da1 100644 --- a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64Serializer.java +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64Serializer.java @@ -11,19 +11,21 @@ public class Base64Serializer extends JsonSerializer { private static final Base64.Encoder ENCODER = Base64.getUrlEncoder().withoutPadding(); - private static final String EMPTY = ""; @Override public void serialize(final byte[] value, final JsonGenerator generator, final SerializerProvider provider) throws IOException { - final Optional optional = Optional.ofNullable(value); - if (optional.isPresent()) { - final String text = optional - .filter(v -> v.length > 0) - .map(ENCODER::encodeToString) - .orElse(EMPTY); + final String text = base64Encode(value); + if (text != null) { generator.writeString(text); } else { generator.writeNull(); } } + + protected String base64Encode(final byte[] value) { + final Optional optional = Optional.ofNullable(value); + return optional.filter(v -> v.length > 0) + .map(ENCODER::encodeToString) + .orElse(null); + } } diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64ZipKeyDeserializer.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64ZipKeyDeserializer.java new file mode 100644 index 00000000..d250bafc --- /dev/null +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64ZipKeyDeserializer.java @@ -0,0 +1,20 @@ +package com.github.nagyesta.lowkeyvault.model.json.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.KeyBackupList; + +public class Base64ZipKeyDeserializer extends AbstractBase64ZipDeserializer { + + public Base64ZipKeyDeserializer() { + this(new Base64Deserializer(), new ObjectMapper()); + } + + protected Base64ZipKeyDeserializer(final Base64Deserializer base64Deserializer, final ObjectMapper objectMapper) { + super(base64Deserializer, objectMapper); + } + + @Override + protected Class getType() { + return KeyBackupList.class; + } +} diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64ZipKeySerializer.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64ZipKeySerializer.java new file mode 100644 index 00000000..54a07735 --- /dev/null +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64ZipKeySerializer.java @@ -0,0 +1,15 @@ +package com.github.nagyesta.lowkeyvault.model.json.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.KeyBackupList; + +public class Base64ZipKeySerializer extends AbstractBase64ZipSerializer { + + public Base64ZipKeySerializer() { + this(new Base64Serializer(), new ObjectMapper()); + } + + protected Base64ZipKeySerializer(final Base64Serializer base64Serializer, final ObjectMapper objectMapper) { + super(base64Serializer, objectMapper); + } +} diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64ZipSecretDeserializer.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64ZipSecretDeserializer.java new file mode 100644 index 00000000..97d65a0c --- /dev/null +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64ZipSecretDeserializer.java @@ -0,0 +1,21 @@ +package com.github.nagyesta.lowkeyvault.model.json.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.nagyesta.lowkeyvault.model.v7_2.secret.SecretBackupList; + +public class Base64ZipSecretDeserializer extends AbstractBase64ZipDeserializer { + + public Base64ZipSecretDeserializer() { + this(new Base64Deserializer(), new ObjectMapper()); + } + + protected Base64ZipSecretDeserializer(final Base64Deserializer base64Deserializer, + final ObjectMapper objectMapper) { + super(base64Deserializer, objectMapper); + } + + @Override + protected Class getType() { + return SecretBackupList.class; + } +} diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64ZipSecretSerializer.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64ZipSecretSerializer.java new file mode 100644 index 00000000..55e30c6d --- /dev/null +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64ZipSecretSerializer.java @@ -0,0 +1,15 @@ +package com.github.nagyesta.lowkeyvault.model.json.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.nagyesta.lowkeyvault.model.v7_2.secret.SecretBackupList; + +public class Base64ZipSecretSerializer extends AbstractBase64ZipSerializer { + + public Base64ZipSecretSerializer() { + this(new Base64Serializer(), new ObjectMapper()); + } + + protected Base64ZipSecretSerializer(final Base64Serializer base64Serializer, final ObjectMapper objectMapper) { + super(base64Serializer, objectMapper); + } +} diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/common/BaseBackupListItem.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/common/BaseBackupListItem.java new file mode 100644 index 00000000..1b86eaac --- /dev/null +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/common/BaseBackupListItem.java @@ -0,0 +1,41 @@ +package com.github.nagyesta.lowkeyvault.model.v7_2.common; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.nagyesta.lowkeyvault.model.v7_2.BasePropertiesModel; +import lombok.Data; + +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.net.URI; +import java.util.Map; + +/** + * Base list item of backup models. + * + * @param

The type of the properties model. + */ +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class BaseBackupListItem

{ + @NotNull + @JsonProperty("vaultBaseUri") + private URI vaultBaseUri; + @NotNull + @NotBlank + @JsonProperty("entityId") + private String id; + @NotNull + @NotBlank + @JsonProperty("entityVersion") + private String version; + @Valid + @NotNull + @JsonProperty("attributes") + private P attributes; + @JsonProperty("tags") + private Map tags; + @JsonProperty("managed") + private boolean managed; +} diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/common/BaseBackupModel.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/common/BaseBackupModel.java new file mode 100644 index 00000000..eaa01a6f --- /dev/null +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/common/BaseBackupModel.java @@ -0,0 +1,27 @@ +package com.github.nagyesta.lowkeyvault.model.v7_2.common; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.github.nagyesta.lowkeyvault.model.v7_2.BasePropertiesModel; +import lombok.Data; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.util.List; + +/** + * Base class of backup models. + * + * @param

The type of the properties model. + * @param The type of the backup list items. + * @param The wrapper type of the backup list. + */ +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class BaseBackupModel

, BL extends List> { + + @Valid + @NotNull + @Size(min = 1) + private BL value; +} diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/KeyBackupList.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/KeyBackupList.java new file mode 100644 index 00000000..46a5fd5f --- /dev/null +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/KeyBackupList.java @@ -0,0 +1,6 @@ +package com.github.nagyesta.lowkeyvault.model.v7_2.key; + +import java.util.ArrayList; + +public class KeyBackupList extends ArrayList { +} diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/KeyBackupListItem.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/KeyBackupListItem.java new file mode 100644 index 00000000..248e0135 --- /dev/null +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/KeyBackupListItem.java @@ -0,0 +1,21 @@ +package com.github.nagyesta.lowkeyvault.model.v7_2.key; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.nagyesta.lowkeyvault.model.v7_2.common.BaseBackupListItem; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.request.JsonWebKeyImportRequest; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; + +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class KeyBackupListItem extends BaseBackupListItem { + @Valid + @NotNull + @JsonProperty("keyMaterial") + private JsonWebKeyImportRequest keyMaterial; +} diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/KeyBackupModel.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/KeyBackupModel.java new file mode 100644 index 00000000..93dfda41 --- /dev/null +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/KeyBackupModel.java @@ -0,0 +1,28 @@ +package com.github.nagyesta.lowkeyvault.model.v7_2.key; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.github.nagyesta.lowkeyvault.model.json.util.Base64ZipKeyDeserializer; +import com.github.nagyesta.lowkeyvault.model.json.util.Base64ZipKeySerializer; +import com.github.nagyesta.lowkeyvault.model.v7_2.common.BaseBackupModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class KeyBackupModel extends BaseBackupModel { + + @JsonSerialize(using = Base64ZipKeySerializer.class) + @Override + public KeyBackupList getValue() { + return super.getValue(); + } + + @JsonDeserialize(using = Base64ZipKeyDeserializer.class) + @Override + public void setValue(final KeyBackupList value) { + super.setValue(value); + } +} diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/constants/KeyType.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/constants/KeyType.java index 4027684c..2856813b 100644 --- a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/constants/KeyType.java +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/constants/KeyType.java @@ -51,8 +51,8 @@ public > VersionedKeyEntityId createKey( @Override public VersionedKeyEntityId importKey(final KeyVaultFake keyVaultFake, - final VersionedKeyEntityId keyEntityId, - final JsonWebKeyImportRequest input) { + final VersionedKeyEntityId keyEntityId, + final JsonWebKeyImportRequest input) { return keyVaultFake.importEcKeyVersion(keyEntityId, input); } }, @@ -84,8 +84,8 @@ public > VersionedKeyEntityId createKey( @Override public VersionedKeyEntityId importKey(final KeyVaultFake keyVaultFake, - final VersionedKeyEntityId keyEntityId, - final JsonWebKeyImportRequest input) { + final VersionedKeyEntityId keyEntityId, + final JsonWebKeyImportRequest input) { return EC.importKey(keyVaultFake, keyEntityId, input); } }, @@ -116,8 +116,8 @@ public > VersionedKeyEntityId createKey( @Override public VersionedKeyEntityId importKey(final KeyVaultFake keyVaultFake, - final VersionedKeyEntityId keyEntityId, - final JsonWebKeyImportRequest input) { + final VersionedKeyEntityId keyEntityId, + final JsonWebKeyImportRequest input) { return keyVaultFake.importRsaKeyVersion(keyEntityId, input); } }, @@ -149,8 +149,8 @@ public > VersionedKeyEntityId createKey( @Override public VersionedKeyEntityId importKey(final KeyVaultFake keyVaultFake, - final VersionedKeyEntityId keyEntityId, - final JsonWebKeyImportRequest input) { + final VersionedKeyEntityId keyEntityId, + final JsonWebKeyImportRequest input) { return RSA.importKey(keyVaultFake, keyEntityId, input); } }, @@ -181,8 +181,8 @@ public > VersionedKeyEntityId createKey( @Override public VersionedKeyEntityId importKey(final KeyVaultFake keyVaultFake, - final VersionedKeyEntityId keyEntityId, - final JsonWebKeyImportRequest input) { + final VersionedKeyEntityId keyEntityId, + final JsonWebKeyImportRequest input) { return keyVaultFake.importOctKeyVersion(keyEntityId, input); } }, @@ -214,8 +214,8 @@ public > VersionedKeyEntityId createKey( @Override public VersionedKeyEntityId importKey(final KeyVaultFake keyVaultFake, - final VersionedKeyEntityId keyEntityId, - final JsonWebKeyImportRequest input) { + final VersionedKeyEntityId keyEntityId, + final JsonWebKeyImportRequest input) { return OCT.importKey(keyVaultFake, keyEntityId, input); } }; diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/secret/KeyVaultSecretModel.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/secret/KeyVaultSecretModel.java index f0e25c4b..0c92118d 100644 --- a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/secret/KeyVaultSecretModel.java +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/secret/KeyVaultSecretModel.java @@ -13,9 +13,6 @@ public class KeyVaultSecretModel { @JsonProperty("id") private String id; - @JsonProperty("kid") - private String keyId; - @JsonProperty("value") private String value; diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/secret/SecretBackupList.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/secret/SecretBackupList.java new file mode 100644 index 00000000..935e1db8 --- /dev/null +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/secret/SecretBackupList.java @@ -0,0 +1,6 @@ +package com.github.nagyesta.lowkeyvault.model.v7_2.secret; + +import java.util.ArrayList; + +public class SecretBackupList extends ArrayList { +} diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/secret/SecretBackupListItem.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/secret/SecretBackupListItem.java new file mode 100644 index 00000000..eacb440b --- /dev/null +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/secret/SecretBackupListItem.java @@ -0,0 +1,21 @@ +package com.github.nagyesta.lowkeyvault.model.v7_2.secret; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.nagyesta.lowkeyvault.model.v7_2.common.BaseBackupListItem; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SecretBackupListItem extends BaseBackupListItem { + @NotNull + @JsonProperty("value") + private String value; + @JsonProperty("contentType") + private String contentType; + +} diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/secret/SecretBackupModel.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/secret/SecretBackupModel.java new file mode 100644 index 00000000..9c36109a --- /dev/null +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/secret/SecretBackupModel.java @@ -0,0 +1,28 @@ +package com.github.nagyesta.lowkeyvault.model.v7_2.secret; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.github.nagyesta.lowkeyvault.model.json.util.Base64ZipSecretDeserializer; +import com.github.nagyesta.lowkeyvault.model.json.util.Base64ZipSecretSerializer; +import com.github.nagyesta.lowkeyvault.model.v7_2.common.BaseBackupModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SecretBackupModel extends BaseBackupModel { + + @JsonSerialize(using = Base64ZipSecretSerializer.class) + @Override + public SecretBackupList getValue() { + return super.getValue(); + } + + @JsonDeserialize(using = Base64ZipSecretDeserializer.class) + @Override + public void setValue(final SecretBackupList value) { + super.setValue(value); + } +} diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/secret/SecretEntityToV72BackupConverter.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/secret/SecretEntityToV72BackupConverter.java new file mode 100644 index 00000000..5ab217e4 --- /dev/null +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/secret/SecretEntityToV72BackupConverter.java @@ -0,0 +1,28 @@ +package com.github.nagyesta.lowkeyvault.model.v7_2.secret; + +import com.github.nagyesta.lowkeyvault.mapper.common.BackupConverter; +import com.github.nagyesta.lowkeyvault.service.secret.ReadOnlyKeyVaultSecretEntity; +import com.github.nagyesta.lowkeyvault.service.secret.id.VersionedSecretEntityId; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.convert.converter.Converter; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; + +@Component +public class SecretEntityToV72BackupConverter + extends BackupConverter { + + @Autowired + public SecretEntityToV72BackupConverter( + @NonNull final Converter propertiesConverter) { + super(propertiesConverter); + } + + @Override + protected SecretBackupListItem convertUniqueFields(@NonNull final ReadOnlyKeyVaultSecretEntity source) { + final SecretBackupListItem listItem = new SecretBackupListItem(); + listItem.setValue(source.getValue()); + listItem.setContentType(source.getContentType()); + return listItem; + } +} diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/common/BaseVaultEntity.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/common/BaseVaultEntity.java index 78355f97..8e0d9968 100644 --- a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/common/BaseVaultEntity.java +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/common/BaseVaultEntity.java @@ -53,4 +53,10 @@ public interface BaseVaultEntity { boolean canPurge(); void timeShift(int offsetSeconds); + + boolean isManaged(); + + void setCreatedOn(OffsetDateTime createdOn); + + void setUpdatedOn(OffsetDateTime updatedOn); } diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/common/impl/KeyVaultBaseEntity.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/common/impl/KeyVaultBaseEntity.java index a8781170..a16c44d0 100644 --- a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/common/impl/KeyVaultBaseEntity.java +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/common/impl/KeyVaultBaseEntity.java @@ -18,6 +18,7 @@ public abstract class KeyVaultBaseEntity extends KeyVaultLif private Map tags; private Optional deletedDate; private Optional scheduledPurgeDate; + private boolean managed; protected KeyVaultBaseEntity(@NonNull final VaultFake vault) { super(); @@ -87,4 +88,13 @@ public void timeShift(final int offsetSeconds) { deletedDate = deletedDate.map(offsetDateTime -> offsetDateTime.minusSeconds(offsetSeconds)); scheduledPurgeDate = scheduledPurgeDate.map(offsetDateTime -> offsetDateTime.minusSeconds(offsetSeconds)); } + + @Override + public boolean isManaged() { + return managed; + } + + public void setManaged(final boolean managed) { + this.managed = managed; + } } diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/common/impl/KeyVaultLifecycleAwareEntity.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/common/impl/KeyVaultLifecycleAwareEntity.java index f3f569f5..c4f12100 100644 --- a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/common/impl/KeyVaultLifecycleAwareEntity.java +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/common/impl/KeyVaultLifecycleAwareEntity.java @@ -1,5 +1,6 @@ package com.github.nagyesta.lowkeyvault.service.common.impl; +import org.springframework.lang.NonNull; import org.springframework.util.Assert; import java.time.OffsetDateTime; @@ -62,6 +63,14 @@ public void timeShift(final int offsetSeconds) { expiry = expiry.map(offsetDateTime -> offsetDateTime.minusSeconds(offsetSeconds)); } + public void setCreatedOn(@NonNull final OffsetDateTime createdOn) { + this.created = createdOn; + } + + public void setUpdatedOn(@NonNull final OffsetDateTime updatedOn) { + this.updated = updatedOn; + } + protected void updatedNow() { this.updated = now(); } diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/key/KeyVaultFake.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/key/KeyVaultFake.java index a6f60fa8..17d8f9d2 100644 --- a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/key/KeyVaultFake.java +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/key/KeyVaultFake.java @@ -27,6 +27,8 @@ public interface KeyVaultFake extends BaseVaultFake> VersionedKeyEntityId createKeyVersion( public VersionedKeyEntityId importKeyVersion( final String keyName, final JsonWebKeyImportRequest key) { final VersionedKeyEntityId keyEntityId = new VersionedKeyEntityId(vaultFake().baseUri(), keyName); + return importKeyVersion(keyEntityId, key); + } + @Override + public VersionedKeyEntityId importKeyVersion( + final VersionedKeyEntityId keyEntityId, final JsonWebKeyImportRequest key) { final KeyType keyType = Objects.requireNonNull(key).getKeyType(); return keyType.importKey(this, keyEntityId, key); } diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/key/impl/RsaKeyVaultKeyEntity.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/key/impl/RsaKeyVaultKeyEntity.java index 835c499f..365b6024 100644 --- a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/key/impl/RsaKeyVaultKeyEntity.java +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/key/impl/RsaKeyVaultKeyEntity.java @@ -16,6 +16,7 @@ import java.math.BigInteger; import java.security.KeyPair; import java.security.Signature; +import java.security.interfaces.RSAPrivateCrtKey; import java.security.interfaces.RSAPublicKey; import static com.github.nagyesta.lowkeyvault.service.key.util.KeyGenUtil.generateRsa; @@ -58,6 +59,36 @@ public byte[] getE() { return ((RSAPublicKey) getKey().getPublic()).getPublicExponent().toByteArray(); } + @Override + public byte[] getD() { + return ((RSAPrivateCrtKey) getKey().getPrivate()).getPrivateExponent().toByteArray(); + } + + @Override + public byte[] getDp() { + return ((RSAPrivateCrtKey) getKey().getPrivate()).getPrimeExponentP().toByteArray(); + } + + @Override + public byte[] getDq() { + return ((RSAPrivateCrtKey) getKey().getPrivate()).getPrimeExponentQ().toByteArray(); + } + + @Override + public byte[] getP() { + return ((RSAPrivateCrtKey) getKey().getPrivate()).getPrimeP().toByteArray(); + } + + @Override + public byte[] getQ() { + return ((RSAPrivateCrtKey) getKey().getPrivate()).getPrimeQ().toByteArray(); + } + + @Override + public byte[] getQi() { + return ((RSAPrivateCrtKey) getKey().getPrivate()).getCrtCoefficient().toByteArray(); + } + @Override public int getKeySize() { return getKeyParam(); diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/secret/SecretVaultFake.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/secret/SecretVaultFake.java index 04725765..b58e879f 100644 --- a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/secret/SecretVaultFake.java +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/secret/SecretVaultFake.java @@ -7,4 +7,6 @@ public interface SecretVaultFake extends BaseVaultFake { VersionedSecretEntityId createSecretVersion(String secretName, String value, String contentType); + + VersionedSecretEntityId createSecretVersion(VersionedSecretEntityId entityId, String value, String contentType); } diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/secret/impl/SecretVaultFakeImpl.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/secret/impl/SecretVaultFakeImpl.java index 0a4ec27e..dd97ef2e 100644 --- a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/secret/impl/SecretVaultFakeImpl.java +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/secret/impl/SecretVaultFakeImpl.java @@ -28,6 +28,12 @@ protected VersionedSecretEntityId createVersionedId(final String id, final Strin public VersionedSecretEntityId createSecretVersion( @NonNull final String secretName, @NonNull final String value, final String contentType) { final VersionedSecretEntityId entityId = new VersionedSecretEntityId(vaultFake().baseUri(), secretName); + return createSecretVersion(entityId, value, contentType); + } + + @Override + public VersionedSecretEntityId createSecretVersion( + @NonNull final VersionedSecretEntityId entityId, @NonNull final String value, final String contentType) { final KeyVaultSecretEntity secretEntity = new KeyVaultSecretEntity(entityId, vaultFake(), value, contentType); return addVersion(entityId, secretEntity); } diff --git a/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/controller/v7_2/KeyBackupRestoreControllerIntegrationTest.java b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/controller/v7_2/KeyBackupRestoreControllerIntegrationTest.java new file mode 100644 index 00000000..a3e0ef3e --- /dev/null +++ b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/controller/v7_2/KeyBackupRestoreControllerIntegrationTest.java @@ -0,0 +1,274 @@ +package com.github.nagyesta.lowkeyvault.controller.v7_2; + +import com.github.nagyesta.lowkeyvault.TestConstantsUri; +import com.github.nagyesta.lowkeyvault.mapper.v7_2.key.KeyEntityToV72BackupConverter; +import com.github.nagyesta.lowkeyvault.mapper.v7_2.key.KeyEntityToV72ModelConverter; +import com.github.nagyesta.lowkeyvault.model.v7_2.common.constants.RecoveryLevel; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.*; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.KeyCurveName; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.KeyOperation; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.KeyType; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.request.JsonWebKeyImportRequest; +import com.github.nagyesta.lowkeyvault.service.exception.NotFoundException; +import com.github.nagyesta.lowkeyvault.service.key.KeyVaultFake; +import com.github.nagyesta.lowkeyvault.service.key.id.VersionedKeyEntityId; +import com.github.nagyesta.lowkeyvault.service.key.impl.EcKeyCreationInput; +import com.github.nagyesta.lowkeyvault.service.key.util.KeyGenUtil; +import com.github.nagyesta.lowkeyvault.service.vault.VaultService; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import java.net.URI; +import java.security.KeyPair; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Stream; + +import static com.github.nagyesta.lowkeyvault.TestConstants.*; +import static com.github.nagyesta.lowkeyvault.TestConstantsKeys.*; +import static org.mockito.Mockito.mock; + +@SpringBootTest +class KeyBackupRestoreControllerIntegrationTest { + + @Autowired + private KeyBackupRestoreController underTest; + @Autowired + private VaultService vaultService; + private URI uri; + + public static Stream nullProvider() { + return Stream.builder() + .add(Arguments.of(null, null, null)) + .add(Arguments.of(mock(KeyEntityToV72ModelConverter.class), null, null)) + .add(Arguments.of(null, mock(KeyEntityToV72BackupConverter.class), null)) + .add(Arguments.of(null, null, mock(VaultService.class))) + .add(Arguments.of(null, mock(KeyEntityToV72BackupConverter.class), null)) + .add(Arguments.of(mock(KeyEntityToV72ModelConverter.class), null, mock(VaultService.class))) + .add(Arguments.of(mock(KeyEntityToV72ModelConverter.class), mock(KeyEntityToV72BackupConverter.class), null)) + .build(); + } + + @BeforeEach + void setUp() { + final String name = UUID.randomUUID().toString(); + uri = URI.create("https://" + name + ".localhost"); + vaultService.create(uri, RecoveryLevel.RECOVERABLE_AND_PURGEABLE, RecoveryLevel.MAX_RECOVERABLE_DAYS_INCLUSIVE); + } + + @AfterEach + void tearDown() { + vaultService.delete(uri); + vaultService.purge(uri); + } + + @ParameterizedTest + @MethodSource("nullProvider") + void testConstructorShouldThrowExceptionWhenCalledWithNulls( + final KeyEntityToV72ModelConverter modelConverter, + final KeyEntityToV72BackupConverter backupConverter, + final VaultService vaultService) { + //given + + //when + Assertions.assertThrows(IllegalArgumentException.class, + () -> new KeyBackupRestoreController(modelConverter, backupConverter, vaultService)); + + //then + exception + } + + @Test + void testRestoreEntityShouldRestoreASingleKeyWhenCalledWithValidInput() { + //given + final KeyBackupModel backupModel = new KeyBackupModel(); + backupModel.setValue(new KeyBackupList()); + final KeyPair expectedKey = addVersionToList(uri, KEY_NAME_1, KEY_VERSION_1, backupModel, TAGS_THREE_KEYS); + + //when + final ResponseEntity actual = underTest.restore(uri, backupModel); + + //then + Assertions.assertNotNull(actual); + final KeyVaultKeyModel actualBody = actual.getBody(); + Assertions.assertNotNull(actualBody); + Assertions.assertEquals(HttpStatus.OK, actual.getStatusCode()); + assertRestoredKeyMatchesExpectations(actualBody, (ECPublicKey) expectedKey.getPublic(), KEY_VERSION_1, TAGS_THREE_KEYS); + } + + @Test + void testRestoreEntityShouldRestoreAThreeKeysWhenCalledWithValidInput() { + //given + final KeyBackupModel backupModel = new KeyBackupModel(); + backupModel.setValue(new KeyBackupList()); + addVersionToList(uri, KEY_NAME_1, KEY_VERSION_1, backupModel, null); + addVersionToList(uri, KEY_NAME_1, KEY_VERSION_2, backupModel, TAGS_THREE_KEYS); + final KeyPair expectedKey = addVersionToList(uri, KEY_NAME_1, KEY_VERSION_3, backupModel, TAGS_EMPTY); + + //when + final ResponseEntity actual = underTest.restore(uri, backupModel); + + //then + Assertions.assertNotNull(actual); + final KeyVaultKeyModel actualBody = actual.getBody(); + Assertions.assertNotNull(actualBody); + Assertions.assertEquals(HttpStatus.OK, actual.getStatusCode()); + assertRestoredKeyMatchesExpectations(actualBody, (ECPublicKey) expectedKey.getPublic(), KEY_VERSION_3, TAGS_EMPTY); + } + + @Test + void testRestoreEntityShouldThrowExceptionWhenCalledWithMoreThanOneUris() { + //given + final KeyBackupModel backupModel = new KeyBackupModel(); + backupModel.setValue(new KeyBackupList()); + addVersionToList(uri, KEY_NAME_1, KEY_VERSION_1, backupModel, null); + addVersionToList(TestConstantsUri.HTTPS_DEFAULT_LOWKEY_VAULT, KEY_NAME_1, KEY_VERSION_2, backupModel, TAGS_THREE_KEYS); + + //when + Assertions.assertThrows(IllegalArgumentException.class, () -> underTest.restore(uri, backupModel)); + + //then + exception + } + + @Test + void testRestoreEntityShouldThrowExceptionWhenCalledWithMoreThanOneNames() { + //given + final KeyBackupModel backupModel = new KeyBackupModel(); + backupModel.setValue(new KeyBackupList()); + addVersionToList(uri, KEY_NAME_1, KEY_VERSION_1, backupModel, null); + addVersionToList(uri, KEY_NAME_2, KEY_VERSION_2, backupModel, TAGS_THREE_KEYS); + + //when + Assertions.assertThrows(IllegalArgumentException.class, () -> underTest.restore(uri, backupModel)); + + //then + exception + } + + @Test + void testRestoreEntityShouldThrowExceptionWhenCalledWithUnknownUri() { + //given + final KeyBackupModel backupModel = new KeyBackupModel(); + backupModel.setValue(new KeyBackupList()); + addVersionToList(URI.create("https://uknknown.uri"), KEY_NAME_1, KEY_VERSION_1, backupModel, null); + + //when + Assertions.assertThrows(NotFoundException.class, () -> underTest.restore(uri, backupModel)); + + //then + exception + } + + @Test + void testRestoreEntityShouldThrowExceptionWhenNameMatchesActiveKey() { + //given + final KeyBackupModel backupModel = new KeyBackupModel(); + backupModel.setValue(new KeyBackupList()); + addVersionToList(uri, KEY_NAME_1, KEY_VERSION_1, backupModel, TAGS_EMPTY); + addVersionToList(uri, KEY_NAME_1, KEY_VERSION_2, backupModel, TAGS_ONE_KEY); + vaultService.findByUri(uri).keyVaultFake() + .createKeyVersion(KEY_NAME_1, new EcKeyCreationInput(KeyType.EC, KeyCurveName.P_256)); + + //when + Assertions.assertThrows(IllegalArgumentException.class, () -> underTest.restore(uri, backupModel)); + + //then + exception + } + + @Test + void testRestoreEntityShouldThrowExceptionWhenNameMatchesDeletedKey() { + //given + final KeyBackupModel backupModel = new KeyBackupModel(); + backupModel.setValue(new KeyBackupList()); + addVersionToList(uri, KEY_NAME_1, KEY_VERSION_1, backupModel, TAGS_EMPTY); + addVersionToList(uri, KEY_NAME_1, KEY_VERSION_2, backupModel, TAGS_ONE_KEY); + final KeyVaultFake vaultFake = vaultService.findByUri(uri).keyVaultFake(); + final VersionedKeyEntityId keyVersion = vaultFake + .createKeyVersion(KEY_NAME_1, new EcKeyCreationInput(KeyType.EC, KeyCurveName.P_256)); + vaultFake.delete(keyVersion); + + //when + Assertions.assertThrows(IllegalArgumentException.class, () -> underTest.restore(uri, backupModel)); + + //then + exception + } + + @Test + void testBackupEntityShouldReturnTheOriginalBackupModelWhenCalledAfterRestoreEntity() { + //given + final KeyBackupModel backupModel = new KeyBackupModel(); + backupModel.setValue(new KeyBackupList()); + addVersionToList(uri, KEY_NAME_1, KEY_VERSION_1, backupModel, TAGS_EMPTY); + addVersionToList(uri, KEY_NAME_1, KEY_VERSION_2, backupModel, TAGS_ONE_KEY); + underTest.restore(uri, backupModel); + + //when + final ResponseEntity actual = underTest.backup(KEY_NAME_1, uri); + + //then + Assertions.assertNotNull(actual); + final KeyBackupModel actualBody = actual.getBody(); + Assertions.assertNotNull(actualBody); + Assertions.assertEquals(backupModel, actualBody); + Assertions.assertEquals(HttpStatus.OK, actual.getStatusCode()); + } + + private void assertRestoredKeyMatchesExpectations( + final KeyVaultKeyModel actualBody, final ECPublicKey publicKey, + final String version, final Map expectedTags) { + Assertions.assertEquals(new VersionedKeyEntityId(uri, KEY_NAME_1, version).asUri().toString(), actualBody.getKey().getId()); + Assertions.assertEquals(KeyCurveName.P_256, actualBody.getKey().getCurveName()); + Assertions.assertEquals(KeyType.EC, actualBody.getKey().getKeyType()); + Assertions.assertIterableEquals(List.of(KeyOperation.SIGN, KeyOperation.VERIFY), actualBody.getKey().getKeyOps()); + Assertions.assertArrayEquals(publicKey.getW().getAffineX().toByteArray(), actualBody.getKey().getX()); + Assertions.assertArrayEquals(publicKey.getW().getAffineY().toByteArray(), actualBody.getKey().getY()); + //do not return private key material in response + Assertions.assertNull(actualBody.getKey().getD()); + Assertions.assertEquals(TIME_10_MINUTES_AGO, actualBody.getAttributes().getCreatedOn()); + Assertions.assertEquals(NOW, actualBody.getAttributes().getUpdatedOn()); + Assertions.assertEquals(TIME_IN_10_MINUTES, actualBody.getAttributes().getNotBefore()); + Assertions.assertEquals(TIME_IN_10_MINUTES.plusDays(1), actualBody.getAttributes().getExpiresOn()); + Assertions.assertEquals(RecoveryLevel.RECOVERABLE_AND_PURGEABLE, actualBody.getAttributes().getRecoveryLevel()); + Assertions.assertEquals(RecoveryLevel.MAX_RECOVERABLE_DAYS_INCLUSIVE, actualBody.getAttributes().getRecoverableDays()); + Assertions.assertTrue(actualBody.getAttributes().isEnabled()); + Assertions.assertEquals(expectedTags, actualBody.getTags()); + } + + private KeyPair addVersionToList(final URI baseUri, final String name, final String version, + final KeyBackupModel backupModel, final Map tags) { + final KeyPair keyPair = KeyGenUtil.generateEc(KeyCurveName.P_256); + final JsonWebKeyImportRequest keyMaterial = new JsonWebKeyImportRequest(); + keyMaterial.setKeyType(KeyType.EC); + keyMaterial.setCurveName(KeyCurveName.P_256); + keyMaterial.setKeyOps(List.of(KeyOperation.SIGN, KeyOperation.VERIFY)); + keyMaterial.setD(((ECPrivateKey) keyPair.getPrivate()).getS().toByteArray()); + keyMaterial.setX(((ECPublicKey) keyPair.getPublic()).getW().getAffineX().toByteArray()); + keyMaterial.setY(((ECPublicKey) keyPair.getPublic()).getW().getAffineY().toByteArray()); + keyMaterial.setId(new VersionedKeyEntityId(baseUri, name, version).asUri().toString()); + final KeyBackupListItem listItem = new KeyBackupListItem(); + listItem.setKeyMaterial(keyMaterial); + listItem.setVaultBaseUri(baseUri); + listItem.setId(name); + listItem.setVersion(version); + final KeyPropertiesModel propertiesModel = new KeyPropertiesModel(); + propertiesModel.setCreatedOn(TIME_10_MINUTES_AGO); + propertiesModel.setUpdatedOn(NOW); + propertiesModel.setNotBefore(TIME_IN_10_MINUTES); + propertiesModel.setExpiresOn(TIME_IN_10_MINUTES.plusDays(1)); + propertiesModel.setRecoveryLevel(RecoveryLevel.RECOVERABLE_AND_PURGEABLE); + propertiesModel.setRecoverableDays(RecoveryLevel.MAX_RECOVERABLE_DAYS_INCLUSIVE); + listItem.setAttributes(propertiesModel); + listItem.setTags(tags); + backupModel.getValue().add(listItem); + return keyPair; + } +} diff --git a/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/controller/v7_2/SecretBackupRestoreControllerIntegrationTest.java b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/controller/v7_2/SecretBackupRestoreControllerIntegrationTest.java new file mode 100644 index 00000000..d5104f9f --- /dev/null +++ b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/controller/v7_2/SecretBackupRestoreControllerIntegrationTest.java @@ -0,0 +1,247 @@ +package com.github.nagyesta.lowkeyvault.controller.v7_2; + +import com.github.nagyesta.lowkeyvault.TestConstantsUri; +import com.github.nagyesta.lowkeyvault.mapper.v7_2.secret.SecretEntityToV72ModelConverter; +import com.github.nagyesta.lowkeyvault.model.v7_2.common.constants.RecoveryLevel; +import com.github.nagyesta.lowkeyvault.model.v7_2.secret.*; +import com.github.nagyesta.lowkeyvault.service.exception.NotFoundException; +import com.github.nagyesta.lowkeyvault.service.secret.SecretVaultFake; +import com.github.nagyesta.lowkeyvault.service.secret.id.VersionedSecretEntityId; +import com.github.nagyesta.lowkeyvault.service.vault.VaultService; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.util.MimeTypeUtils; + +import java.net.URI; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Stream; + +import static com.github.nagyesta.lowkeyvault.TestConstants.*; +import static com.github.nagyesta.lowkeyvault.TestConstantsSecrets.*; +import static org.mockito.Mockito.mock; + +@SpringBootTest +class SecretBackupRestoreControllerIntegrationTest { + + @Autowired + private SecretBackupRestoreController underTest; + @Autowired + private VaultService vaultService; + private URI uri; + + public static Stream nullProvider() { + return Stream.builder() + .add(Arguments.of(null, null, null)) + .add(Arguments.of(mock(SecretEntityToV72ModelConverter.class), null, null)) + .add(Arguments.of(null, mock(SecretEntityToV72BackupConverter.class), null)) + .add(Arguments.of(null, null, mock(VaultService.class))) + .add(Arguments.of(null, mock(SecretEntityToV72BackupConverter.class), null)) + .add(Arguments.of(mock(SecretEntityToV72ModelConverter.class), null, mock(VaultService.class))) + .add(Arguments.of(mock(SecretEntityToV72ModelConverter.class), mock(SecretEntityToV72BackupConverter.class), null)) + .build(); + } + + @BeforeEach + void setUp() { + final String name = UUID.randomUUID().toString(); + uri = URI.create("https://" + name + ".localhost"); + vaultService.create(uri, RecoveryLevel.RECOVERABLE_AND_PURGEABLE, RecoveryLevel.MAX_RECOVERABLE_DAYS_INCLUSIVE); + } + + @AfterEach + void tearDown() { + vaultService.delete(uri); + vaultService.purge(uri); + } + + @ParameterizedTest + @MethodSource("nullProvider") + void testConstructorShouldThrowExceptionWhenCalledWithNulls( + final SecretEntityToV72ModelConverter modelConverter, + final SecretEntityToV72BackupConverter backupConverter, + final VaultService vaultService) { + //given + + //when + Assertions.assertThrows(IllegalArgumentException.class, + () -> new SecretBackupRestoreController(modelConverter, backupConverter, vaultService)); + + //then + exception + } + + @Test + void testRestoreEntityShouldRestoreASingleSecretWhenCalledWithValidInput() { + //given + final SecretBackupModel backupModel = new SecretBackupModel(); + backupModel.setValue(new SecretBackupList()); + addVersionToList(uri, SECRET_NAME_1, SECRET_VERSION_1, backupModel, TAGS_THREE_KEYS); + + //when + final ResponseEntity actual = underTest.restore(uri, backupModel); + + //then + Assertions.assertNotNull(actual); + final KeyVaultSecretModel actualBody = actual.getBody(); + Assertions.assertNotNull(actualBody); + Assertions.assertEquals(HttpStatus.OK, actual.getStatusCode()); + assertRestoredSecretMatchesExpectations(actualBody, SECRET_VERSION_1, TAGS_THREE_KEYS); + } + + @Test + void testRestoreEntityShouldRestoreAThreeSecretsWhenCalledWithValidInput() { + //given + final SecretBackupModel backupModel = new SecretBackupModel(); + backupModel.setValue(new SecretBackupList()); + addVersionToList(uri, SECRET_NAME_1, SECRET_VERSION_1, backupModel, null); + addVersionToList(uri, SECRET_NAME_1, SECRET_VERSION_2, backupModel, TAGS_THREE_KEYS); + addVersionToList(uri, SECRET_NAME_1, SECRET_VERSION_3, backupModel, TAGS_EMPTY); + + //when + final ResponseEntity actual = underTest.restore(uri, backupModel); + + //then + Assertions.assertNotNull(actual); + final KeyVaultSecretModel actualBody = actual.getBody(); + Assertions.assertNotNull(actualBody); + Assertions.assertEquals(HttpStatus.OK, actual.getStatusCode()); + assertRestoredSecretMatchesExpectations(actualBody, SECRET_VERSION_3, TAGS_EMPTY); + } + + @Test + void testRestoreEntityShouldThrowExceptionWhenCalledWithMoreThanOneUris() { + //given + final SecretBackupModel backupModel = new SecretBackupModel(); + backupModel.setValue(new SecretBackupList()); + addVersionToList(uri, SECRET_NAME_1, SECRET_VERSION_1, backupModel, null); + addVersionToList(TestConstantsUri.HTTPS_DEFAULT_LOWKEY_VAULT, SECRET_NAME_1, SECRET_VERSION_2, backupModel, TAGS_THREE_KEYS); + + //when + Assertions.assertThrows(IllegalArgumentException.class, () -> underTest.restore(uri, backupModel)); + + //then + exception + } + + @Test + void testRestoreEntityShouldThrowExceptionWhenCalledWithMoreThanOneNames() { + //given + final SecretBackupModel backupModel = new SecretBackupModel(); + backupModel.setValue(new SecretBackupList()); + addVersionToList(uri, SECRET_NAME_1, SECRET_VERSION_1, backupModel, null); + addVersionToList(uri, SECRET_NAME_2, SECRET_VERSION_2, backupModel, TAGS_THREE_KEYS); + + //when + Assertions.assertThrows(IllegalArgumentException.class, () -> underTest.restore(uri, backupModel)); + + //then + exception + } + + @Test + void testRestoreEntityShouldThrowExceptionWhenCalledWithUnknownUri() { + //given + final SecretBackupModel backupModel = new SecretBackupModel(); + backupModel.setValue(new SecretBackupList()); + addVersionToList(URI.create("https://uknknown.uri"), SECRET_NAME_1, SECRET_VERSION_1, backupModel, null); + + //when + Assertions.assertThrows(NotFoundException.class, () -> underTest.restore(uri, backupModel)); + + //then + exception + } + + @Test + void testRestoreEntityShouldThrowExceptionWhenNameMatchesActiveSecret() { + //given + final SecretBackupModel backupModel = new SecretBackupModel(); + backupModel.setValue(new SecretBackupList()); + addVersionToList(uri, SECRET_NAME_1, SECRET_VERSION_1, backupModel, TAGS_EMPTY); + addVersionToList(uri, SECRET_NAME_1, SECRET_VERSION_2, backupModel, TAGS_ONE_KEY); + vaultService.findByUri(uri).secretVaultFake().createSecretVersion(SECRET_NAME_1, LOWKEY_VAULT, null); + + //when + Assertions.assertThrows(IllegalArgumentException.class, () -> underTest.restore(uri, backupModel)); + + //then + exception + } + + @Test + void testRestoreEntityShouldThrowExceptionWhenNameMatchesDeletedSecret() { + //given + final SecretBackupModel backupModel = new SecretBackupModel(); + backupModel.setValue(new SecretBackupList()); + addVersionToList(uri, SECRET_NAME_1, SECRET_VERSION_1, backupModel, TAGS_EMPTY); + addVersionToList(uri, SECRET_NAME_1, SECRET_VERSION_2, backupModel, TAGS_ONE_KEY); + final SecretVaultFake vaultFake = vaultService.findByUri(uri).secretVaultFake(); + final VersionedSecretEntityId secretVersion = vaultFake.createSecretVersion(SECRET_NAME_1, LOWKEY_VAULT, null); + vaultFake.delete(secretVersion); + + //when + Assertions.assertThrows(IllegalArgumentException.class, () -> underTest.restore(uri, backupModel)); + + //then + exception + } + + @Test + void testBackupEntityShouldReturnTheOriginalBackupModelWhenCalledAfterRestoreEntity() { + //given + final SecretBackupModel backupModel = new SecretBackupModel(); + backupModel.setValue(new SecretBackupList()); + addVersionToList(uri, SECRET_NAME_1, SECRET_VERSION_1, backupModel, TAGS_EMPTY); + addVersionToList(uri, SECRET_NAME_1, SECRET_VERSION_2, backupModel, TAGS_ONE_KEY); + underTest.restore(uri, backupModel); + + //when + final ResponseEntity actual = underTest.backup(SECRET_NAME_1, uri); + + //then + Assertions.assertNotNull(actual); + final SecretBackupModel actualBody = actual.getBody(); + Assertions.assertNotNull(actualBody); + Assertions.assertEquals(backupModel, actualBody); + Assertions.assertEquals(HttpStatus.OK, actual.getStatusCode()); + } + + private void assertRestoredSecretMatchesExpectations( + final KeyVaultSecretModel actualBody, final String version, final Map expectedTags) { + Assertions.assertEquals(LOWKEY_VAULT, actualBody.getValue()); + Assertions.assertEquals(MimeTypeUtils.TEXT_PLAIN_VALUE, actualBody.getContentType()); + Assertions.assertEquals(new VersionedSecretEntityId(uri, SECRET_NAME_1, version).asUri().toString(), actualBody.getId()); + Assertions.assertEquals(TIME_10_MINUTES_AGO, actualBody.getAttributes().getCreatedOn()); + Assertions.assertEquals(NOW, actualBody.getAttributes().getUpdatedOn()); + Assertions.assertEquals(TIME_IN_10_MINUTES, actualBody.getAttributes().getNotBefore()); + Assertions.assertEquals(TIME_IN_10_MINUTES.plusDays(1), actualBody.getAttributes().getExpiresOn()); + Assertions.assertEquals(RecoveryLevel.RECOVERABLE_AND_PURGEABLE, actualBody.getAttributes().getRecoveryLevel()); + Assertions.assertEquals(RecoveryLevel.MAX_RECOVERABLE_DAYS_INCLUSIVE, actualBody.getAttributes().getRecoverableDays()); + Assertions.assertTrue(actualBody.getAttributes().isEnabled()); + Assertions.assertEquals(expectedTags, actualBody.getTags()); + } + + private void addVersionToList(final URI baseUri, final String name, final String version, + final SecretBackupModel backupModel, final Map tags) { + final SecretBackupListItem listItem = new SecretBackupListItem(); + listItem.setValue(LOWKEY_VAULT); + listItem.setContentType(MimeTypeUtils.TEXT_PLAIN_VALUE); + listItem.setVaultBaseUri(baseUri); + listItem.setId(name); + listItem.setVersion(version); + final SecretPropertiesModel propertiesModel = new SecretPropertiesModel(); + propertiesModel.setCreatedOn(TIME_10_MINUTES_AGO); + propertiesModel.setUpdatedOn(NOW); + propertiesModel.setNotBefore(TIME_IN_10_MINUTES); + propertiesModel.setExpiresOn(TIME_IN_10_MINUTES.plusDays(1)); + propertiesModel.setRecoveryLevel(RecoveryLevel.RECOVERABLE_AND_PURGEABLE); + propertiesModel.setRecoverableDays(RecoveryLevel.MAX_RECOVERABLE_DAYS_INCLUSIVE); + listItem.setAttributes(propertiesModel); + listItem.setTags(tags); + backupModel.getValue().add(listItem); + } +} diff --git a/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/mapper/v7_2/key/KeyEntityToV72ModelConverterTest.java b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/mapper/v7_2/key/KeyEntityToV72ModelConverterTest.java index d79e5e28..811e645e 100644 --- a/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/mapper/v7_2/key/KeyEntityToV72ModelConverterTest.java +++ b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/mapper/v7_2/key/KeyEntityToV72ModelConverterTest.java @@ -173,7 +173,7 @@ void testConvertShouldConvertAllOctFieldsWhenCalledWithOctKeyEntity( } else { Assertions.fail("Only HSM is supported, software protection isn't."); } - Assertions.assertArrayEquals(input.getK(), actual.getKey().getK()); + Assertions.assertNull(actual.getKey().getK()); assertRsaFieldsAreNull(actual); assertEcFieldsAreNull(actual); diff --git a/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64SerializerTest.java b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64SerializerTest.java index a5ac6425..659c91c7 100644 --- a/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64SerializerTest.java +++ b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64SerializerTest.java @@ -37,7 +37,7 @@ public static Stream base64Provider() { final Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding(); return Stream.of(null, EMPTY, BLANK, LOCALHOST) .map(s -> Optional.ofNullable(s).map(String::getBytes).orElse(null)) - .map(b -> Arguments.of(b, Optional.ofNullable(b).map(encoder::encodeToString).orElse(null))); + .map(b -> Arguments.of(b, Optional.ofNullable(b).filter(v -> v.length > 0).map(encoder::encodeToString).orElse(null))); } @BeforeEach diff --git a/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64ZipKeyDeserializerTest.java b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64ZipKeyDeserializerTest.java new file mode 100644 index 00000000..10a494d9 --- /dev/null +++ b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64ZipKeyDeserializerTest.java @@ -0,0 +1,49 @@ +package com.github.nagyesta.lowkeyvault.model.json.util; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.KeyBackupList; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.mockito.Mockito.*; + +class Base64ZipKeyDeserializerTest { + + @Test + void testDeserializeShouldThrowExceptionWhenDecodingFails() throws IOException { + //given + final Base64Deserializer base64Deserializer = mock(Base64Deserializer.class); + final ObjectMapper objectMapper = mock(ObjectMapper.class); + final Base64ZipKeyDeserializer underTest = new Base64ZipKeyDeserializer(base64Deserializer, objectMapper); + final JsonParser jsonParser = mock(JsonParser.class); + final DeserializationContext context = mock(DeserializationContext.class); + when(base64Deserializer.deserializeBase64(eq(jsonParser))).thenReturn(new byte[1]); + when(objectMapper.reader()).thenThrow(new IllegalStateException("Fail")); + + //when + Assertions.assertThrows(IllegalArgumentException.class, () -> underTest.deserialize(jsonParser, context)); + + //then + exception + verify(base64Deserializer).deserializeBase64(eq(jsonParser)); + } + + @Test + void testDeserializeShouldWriteNullWhenCalledWithNullInput() throws IOException { + //given + final Base64ZipKeyDeserializer underTest = new Base64ZipKeyDeserializer(); + final JsonParser jsonParser = mock(JsonParser.class); + when(jsonParser.readValueAs(eq(String.class))).thenReturn(""); + final DeserializationContext context = mock(DeserializationContext.class); + + //when + final KeyBackupList actual = underTest.deserialize(jsonParser, context); + + //then + Assertions.assertNull(actual); + verify(jsonParser).readValueAs(eq(String.class)); + } +} diff --git a/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64ZipKeySerializerDeserializerIntegrationTest.java b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64ZipKeySerializerDeserializerIntegrationTest.java new file mode 100644 index 00000000..6d9baee5 --- /dev/null +++ b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64ZipKeySerializerDeserializerIntegrationTest.java @@ -0,0 +1,121 @@ +package com.github.nagyesta.lowkeyvault.model.json.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.nagyesta.lowkeyvault.TestConstantsKeys; +import com.github.nagyesta.lowkeyvault.model.v7_2.common.constants.RecoveryLevel; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.KeyBackupList; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.KeyBackupListItem; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.KeyBackupModel; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.KeyPropertiesModel; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.KeyCurveName; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.KeyType; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.request.JsonWebKeyImportRequest; +import com.github.nagyesta.lowkeyvault.service.key.id.KeyEntityId; +import com.github.nagyesta.lowkeyvault.service.key.util.KeyGenUtil; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.io.IOException; +import java.security.KeyPair; +import java.util.Map; + +import static com.github.nagyesta.lowkeyvault.TestConstants.*; + +@SpringBootTest +class Base64ZipKeySerializerDeserializerIntegrationTest { + + @Autowired + private ObjectMapper objectMapper; + + @Test + void testSerializeShouldReturnNullWhenCalledWithNull() throws IOException { + //given + + //when + final String json = objectMapper.writerFor(KeyBackupModel.class).writeValueAsString(null); + final KeyBackupModel actual = objectMapper.reader().readValue(json, KeyBackupModel.class); + + //then + Assertions.assertNull(actual); + } + + @Test + void testSerializeShouldReturnNullWhenCalledWithNullList() throws IOException { + //given + final KeyBackupModel valueWithNullList = new KeyBackupModel(); + + //when + final String json = objectMapper.writer().writeValueAsString(valueWithNullList); + final KeyBackupModel actual = objectMapper.reader().readValue(json, KeyBackupModel.class); + + //then + Assertions.assertEquals(valueWithNullList, actual); + } + + @Test + void testSerializeShouldConvertContentWhenCalledWithValidValue() throws IOException { + //given + final KeyBackupListItem item = getKeyBackupListItem(TestConstantsKeys.VERSIONED_KEY_ENTITY_ID_1_VERSION_1, + getKeyMaterial(TestConstantsKeys.VERSIONED_KEY_ENTITY_ID_1_VERSION_1, KeyGenUtil.generateEc(KeyCurveName.P_256)), + getKeyPropertiesModel()); + final KeyBackupModel input = getKeyBackupModel(item); + + //when + final String json = objectMapper.writer().writeValueAsString(input); + final KeyBackupModel actual = objectMapper.reader().readValue(json, KeyBackupModel.class); + + //then + Assertions.assertEquals(input, actual); + } + + private KeyBackupModel getKeyBackupModel(final KeyBackupListItem item) { + final KeyBackupList list = new KeyBackupList(); + list.add(item); + final KeyBackupModel input = new KeyBackupModel(); + input.setValue(list); + return input; + } + + @SuppressWarnings("SameParameterValue") + private KeyBackupListItem getKeyBackupListItem(final KeyEntityId id, + final JsonWebKeyImportRequest keyMaterial, + final KeyPropertiesModel propertiesModel) { + final KeyBackupListItem item = new KeyBackupListItem(); + item.setId(id.id()); + item.setVaultBaseUri(id.vault()); + item.setVersion(id.version()); + item.setKeyMaterial(keyMaterial); + item.setManaged(true); + item.setTags(Map.of(KEY_1, VALUE_1, KEY_2, VALUE_2)); + item.setAttributes(propertiesModel); + return item; + } + + private KeyPropertiesModel getKeyPropertiesModel() { + final KeyPropertiesModel propertiesModel = new KeyPropertiesModel(); + propertiesModel.setCreatedOn(TIME_10_MINUTES_AGO); + propertiesModel.setUpdatedOn(NOW.minusSeconds(1)); + propertiesModel.setNotBefore(NOW); + propertiesModel.setExpiresOn(TIME_IN_10_MINUTES); + propertiesModel.setEnabled(true); + propertiesModel.setRecoveryLevel(RecoveryLevel.PURGEABLE); + propertiesModel.setRecoverableDays(null); + return propertiesModel; + } + + @SuppressWarnings("SameParameterValue") + private JsonWebKeyImportRequest getKeyMaterial(final KeyEntityId id, final KeyPair expected) { + final JsonWebKeyImportRequest keyMaterial = new JsonWebKeyImportRequest(); + keyMaterial.setKeyType(KeyType.EC); + keyMaterial.setX(((BCECPublicKey) expected.getPublic()).getQ().getAffineXCoord().getEncoded()); + keyMaterial.setY(((BCECPublicKey) expected.getPublic()).getQ().getAffineYCoord().getEncoded()); + keyMaterial.setD(((BCECPrivateKey) expected.getPrivate()).getD().toByteArray()); + keyMaterial.setCurveName(KeyCurveName.P_256); + keyMaterial.setId(id.asString()); + return keyMaterial; + } +} diff --git a/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64ZipKeySerializerTest.java b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64ZipKeySerializerTest.java new file mode 100644 index 00000000..ed93b6bb --- /dev/null +++ b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64ZipKeySerializerTest.java @@ -0,0 +1,48 @@ +package com.github.nagyesta.lowkeyvault.model.json.util; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.KeyBackupList; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.mockito.Mockito.*; + +class Base64ZipKeySerializerTest { + + @Test + void testSerializeShouldThrowExceptionWhenEncodingFails() { + //given + final Base64Serializer base64Serializer = mock(Base64Serializer.class); + final ObjectMapper objectMapper = new ObjectMapper(); + final Base64ZipKeySerializer underTest = new Base64ZipKeySerializer(base64Serializer, objectMapper); + final JsonGenerator gen = mock(JsonGenerator.class); + final SerializerProvider serializers = mock(SerializerProvider.class); + when(base64Serializer.base64Encode(any())).thenThrow(new IllegalStateException("Fail")); + + //when + Assertions.assertThrows(IllegalArgumentException.class, () -> underTest.serialize(new KeyBackupList(), gen, serializers)); + + //then + exception + verify(base64Serializer).base64Encode(any()); + verifyNoInteractions(gen, serializers); + } + + @Test + void testSerializeShouldWriteNullWhenCalledWithNullInput() throws IOException { + //given + final Base64ZipKeySerializer underTest = new Base64ZipKeySerializer(); + final JsonGenerator gen = mock(JsonGenerator.class); + final SerializerProvider serializers = mock(SerializerProvider.class); + + //when + underTest.serialize(null, gen, serializers); + + //then + verify(gen).writeNull(); + verify(gen, never()).writeString(anyString()); + } +} diff --git a/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64ZipSecretSerializerDeserializerIntegrationTest.java b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64ZipSecretSerializerDeserializerIntegrationTest.java new file mode 100644 index 00000000..c02d0a20 --- /dev/null +++ b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/model/json/util/Base64ZipSecretSerializerDeserializerIntegrationTest.java @@ -0,0 +1,105 @@ +package com.github.nagyesta.lowkeyvault.model.json.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.nagyesta.lowkeyvault.TestConstantsSecrets; +import com.github.nagyesta.lowkeyvault.model.v7_2.common.constants.RecoveryLevel; +import com.github.nagyesta.lowkeyvault.model.v7_2.secret.SecretBackupList; +import com.github.nagyesta.lowkeyvault.model.v7_2.secret.SecretBackupListItem; +import com.github.nagyesta.lowkeyvault.model.v7_2.secret.SecretBackupModel; +import com.github.nagyesta.lowkeyvault.model.v7_2.secret.SecretPropertiesModel; +import com.github.nagyesta.lowkeyvault.service.secret.id.SecretEntityId; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.util.MimeTypeUtils; + +import java.io.IOException; +import java.util.Map; + +import static com.github.nagyesta.lowkeyvault.TestConstants.*; + +@SpringBootTest +class Base64ZipSecretSerializerDeserializerIntegrationTest { + + @Autowired + private ObjectMapper objectMapper; + + @Test + void testSerializeShouldReturnNullWhenCalledWithNull() throws IOException { + //given + + //when + final String json = objectMapper.writerFor(SecretBackupModel.class).writeValueAsString(null); + final SecretBackupModel actual = objectMapper.reader().readValue(json, SecretBackupModel.class); + + //then + Assertions.assertNull(actual); + } + + @Test + void testSerializeShouldReturnNullWhenCalledWithNullList() throws IOException { + //given + final SecretBackupModel valueWithNullList = new SecretBackupModel(); + + //when + final String json = objectMapper.writer().writeValueAsString(valueWithNullList); + final SecretBackupModel actual = objectMapper.reader().readValue(json, SecretBackupModel.class); + + //then + Assertions.assertEquals(valueWithNullList, actual); + } + + @Test + void testSerializeShouldConvertContentWhenCalledWithValidValue() throws IOException { + //given + final SecretBackupListItem item = getSecretBackupListItem(TestConstantsSecrets.VERSIONED_SECRET_ENTITY_ID_1_VERSION_1, + LOWKEY_VAULT, MimeTypeUtils.TEXT_PLAIN_VALUE, + getSecretPropertiesModel()); + final SecretBackupModel input = getSecretBackupModel(item); + + //when + final String json = objectMapper.writer().writeValueAsString(input); + final SecretBackupModel actual = objectMapper.reader().readValue(json, SecretBackupModel.class); + + //then + Assertions.assertEquals(input, actual); + } + + private SecretBackupModel getSecretBackupModel(final SecretBackupListItem item) { + final SecretBackupList list = new SecretBackupList(); + list.add(item); + final SecretBackupModel input = new SecretBackupModel(); + input.setValue(list); + return input; + } + + @SuppressWarnings("SameParameterValue") + private SecretBackupListItem getSecretBackupListItem(final SecretEntityId id, + final String value, + final String contentType, + final SecretPropertiesModel propertiesModel) { + final SecretBackupListItem item = new SecretBackupListItem(); + item.setId(id.id()); + item.setVaultBaseUri(id.vault()); + item.setVersion(id.version()); + item.setValue(value); + item.setContentType(contentType); + item.setManaged(true); + item.setTags(Map.of(KEY_1, VALUE_1, KEY_2, VALUE_2)); + item.setAttributes(propertiesModel); + return item; + } + + private SecretPropertiesModel getSecretPropertiesModel() { + final SecretPropertiesModel propertiesModel = new SecretPropertiesModel(); + propertiesModel.setCreatedOn(TIME_10_MINUTES_AGO); + propertiesModel.setUpdatedOn(NOW.minusSeconds(1)); + propertiesModel.setNotBefore(NOW); + propertiesModel.setExpiresOn(TIME_IN_10_MINUTES); + propertiesModel.setEnabled(true); + propertiesModel.setRecoveryLevel(RecoveryLevel.PURGEABLE); + propertiesModel.setRecoverableDays(null); + return propertiesModel; + } +} diff --git a/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/KeyEntityToV72BackupConverterTest.java b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/KeyEntityToV72BackupConverterTest.java new file mode 100644 index 00000000..fbf47e21 --- /dev/null +++ b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/KeyEntityToV72BackupConverterTest.java @@ -0,0 +1,196 @@ +package com.github.nagyesta.lowkeyvault.model.v7_2.key; + +import com.github.nagyesta.lowkeyvault.mapper.v7_2.key.KeyEntityToV72BackupConverter; +import com.github.nagyesta.lowkeyvault.mapper.v7_2.key.KeyEntityToV72PropertiesModelConverter; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.KeyCurveName; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.KeyType; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.request.JsonWebKeyImportRequest; +import com.github.nagyesta.lowkeyvault.service.key.ReadOnlyKeyVaultKeyEntity; +import com.github.nagyesta.lowkeyvault.service.key.id.VersionedKeyEntityId; +import com.github.nagyesta.lowkeyvault.service.key.impl.AesKeyVaultKeyEntity; +import com.github.nagyesta.lowkeyvault.service.key.impl.EcKeyVaultKeyEntity; +import com.github.nagyesta.lowkeyvault.service.key.impl.RsaKeyVaultKeyEntity; +import com.github.nagyesta.lowkeyvault.service.key.util.KeyGenUtil; +import com.github.nagyesta.lowkeyvault.service.vault.VaultFake; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import javax.crypto.SecretKey; +import java.security.KeyPair; +import java.util.Collections; +import java.util.Map; + +import static com.github.nagyesta.lowkeyvault.TestConstants.KEY_1; +import static com.github.nagyesta.lowkeyvault.TestConstants.VALUE_1; +import static com.github.nagyesta.lowkeyvault.TestConstantsKeys.VERSIONED_KEY_ENTITY_ID_1_VERSION_1; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +class KeyEntityToV72BackupConverterTest { + + private static final KeyPropertiesModel KEY_PROPERTIES_MODEL = new KeyPropertiesModel(); + @Mock + private VaultFake vaultFake; + @Mock + private KeyEntityToV72PropertiesModelConverter propertiesModelConverter; + @InjectMocks + private KeyEntityToV72BackupConverter underTest; + private AutoCloseable openMocks; + + @BeforeEach + void setUp() { + openMocks = MockitoAnnotations.openMocks(this); + when(propertiesModelConverter.convert(any(ReadOnlyKeyVaultKeyEntity.class))).thenReturn(KEY_PROPERTIES_MODEL); + } + + @AfterEach + void tearDown() throws Exception { + openMocks.close(); + } + + @SuppressWarnings("ConstantConditions") + @Test + void testConstructorShouldThrowExceptionWhenCalledWithNull() { + //given + + //when + Assertions.assertThrows(IllegalArgumentException.class, () -> new KeyEntityToV72BackupConverter(null)); + + //then + exception + } + + @Test + void testConvertShouldConvertPopulatedFieldsWhenCalledWithMinimalRsaInput() { + //given + final Integer keySize = KeyType.RSA.getValidKeyParameters(Integer.class).first(); + final KeyPair keyPair = KeyGenUtil.generateRsa(keySize, null); + final RsaKeyVaultKeyEntity input = new RsaKeyVaultKeyEntity( + VERSIONED_KEY_ENTITY_ID_1_VERSION_1, vaultFake, keyPair, keySize, false); + + //when + final KeyBackupListItem actual = underTest.convert(input); + + //then + Assertions.assertNotNull(actual); + final JsonWebKeyImportRequest keyMaterial = actual.getKeyMaterial(); + assertCommonKeyPropertiesAreEqual(input, keyMaterial); + assertRsaPropertiesAreEqual(input, keyMaterial); + assertMinimalPropertiesPopulated(actual); + assertIdsEqual(input.getId(), actual); + verify(propertiesModelConverter).convert(any(ReadOnlyKeyVaultKeyEntity.class)); + verifyNoMoreInteractions(propertiesModelConverter); + } + + @Test + void testConvertShouldConvertPopulatedFieldsWhenCalledWithMinimalAesInput() { + //given + final Integer keySize = KeyType.OCT.getValidKeyParameters(Integer.class).first(); + final SecretKey secretKey = KeyGenUtil.generateAes(keySize); + final AesKeyVaultKeyEntity input = new AesKeyVaultKeyEntity( + VERSIONED_KEY_ENTITY_ID_1_VERSION_1, vaultFake, secretKey, keySize, true); + + //when + final KeyBackupListItem actual = underTest.convert(input); + + //then + Assertions.assertNotNull(actual); + final JsonWebKeyImportRequest keyMaterial = actual.getKeyMaterial(); + assertCommonKeyPropertiesAreEqual(input, keyMaterial); + assertOctPropertiesAreEqual(input, keyMaterial); + assertMinimalPropertiesPopulated(actual); + assertIdsEqual(input.getId(), actual); + verify(propertiesModelConverter).convert(any(ReadOnlyKeyVaultKeyEntity.class)); + verifyNoMoreInteractions(propertiesModelConverter); + } + + @Test + void testConvertShouldConvertPopulatedFieldsWhenCalledWithMinimalEcInput() { + //given + final KeyPair keyPair = KeyGenUtil.generateEc(KeyCurveName.P_256); + final EcKeyVaultKeyEntity input = new EcKeyVaultKeyEntity( + VERSIONED_KEY_ENTITY_ID_1_VERSION_1, vaultFake, keyPair, KeyCurveName.P_256, true); + + //when + final KeyBackupListItem actual = underTest.convert(input); + + //then + Assertions.assertNotNull(actual); + final JsonWebKeyImportRequest keyMaterial = actual.getKeyMaterial(); + assertCommonKeyPropertiesAreEqual(input, keyMaterial); + assertEcPropertiesAreEqual(input, keyMaterial); + assertMinimalPropertiesPopulated(actual); + assertIdsEqual(input.getId(), actual); + verify(propertiesModelConverter).convert(any(ReadOnlyKeyVaultKeyEntity.class)); + verifyNoMoreInteractions(propertiesModelConverter); + } + + @Test + void testConvertShouldConvertAllFieldsWhenCalledWithFullyPopulatedInput() { + //given + final Map tagMap = Map.of(KEY_1, VALUE_1); + final KeyPair keyPair = KeyGenUtil.generateEc(KeyCurveName.P_256); + final EcKeyVaultKeyEntity input = new EcKeyVaultKeyEntity( + VERSIONED_KEY_ENTITY_ID_1_VERSION_1, vaultFake, keyPair, KeyCurveName.P_256, true); + input.setTags(tagMap); + input.setManaged(true); + + //when + final KeyBackupListItem actual = underTest.convert(input); + + //then + Assertions.assertNotNull(actual); + final JsonWebKeyImportRequest keyMaterial = actual.getKeyMaterial(); + assertCommonKeyPropertiesAreEqual(input, keyMaterial); + assertEcPropertiesAreEqual(input, keyMaterial); + Assertions.assertSame(KEY_PROPERTIES_MODEL, actual.getAttributes()); + Assertions.assertEquals(tagMap, actual.getTags()); + Assertions.assertTrue(actual.isManaged()); + assertIdsEqual(input.getId(), actual); + verify(propertiesModelConverter).convert(any(ReadOnlyKeyVaultKeyEntity.class)); + verifyNoMoreInteractions(propertiesModelConverter); + } + + private void assertRsaPropertiesAreEqual(final RsaKeyVaultKeyEntity input, final JsonWebKeyImportRequest keyMaterial) { + Assertions.assertArrayEquals(input.getN(), keyMaterial.getN()); + Assertions.assertArrayEquals(input.getE(), keyMaterial.getE()); + Assertions.assertArrayEquals(input.getD(), keyMaterial.getD()); + Assertions.assertArrayEquals(input.getDp(), keyMaterial.getDp()); + Assertions.assertArrayEquals(input.getDq(), keyMaterial.getDq()); + Assertions.assertArrayEquals(input.getP(), keyMaterial.getP()); + Assertions.assertArrayEquals(input.getQ(), keyMaterial.getQ()); + Assertions.assertArrayEquals(input.getQi(), keyMaterial.getQi()); + } + + private void assertOctPropertiesAreEqual(final AesKeyVaultKeyEntity input, final JsonWebKeyImportRequest keyMaterial) { + Assertions.assertArrayEquals(input.getK(), keyMaterial.getK()); + } + + private void assertEcPropertiesAreEqual(final EcKeyVaultKeyEntity input, final JsonWebKeyImportRequest keyMaterial) { + Assertions.assertArrayEquals(input.getD(), keyMaterial.getD()); + Assertions.assertArrayEquals(input.getX(), keyMaterial.getX()); + Assertions.assertArrayEquals(input.getY(), keyMaterial.getY()); + } + + private void assertMinimalPropertiesPopulated(final KeyBackupListItem actual) { + Assertions.assertSame(KEY_PROPERTIES_MODEL, actual.getAttributes()); + Assertions.assertEquals(Collections.emptyMap(), actual.getTags()); + Assertions.assertFalse(actual.isManaged()); + } + + private void assertCommonKeyPropertiesAreEqual(final ReadOnlyKeyVaultKeyEntity input, final JsonWebKeyImportRequest keyMaterial) { + Assertions.assertNotNull(keyMaterial); + Assertions.assertEquals(input.getKeyType(), keyMaterial.getKeyType()); + Assertions.assertEquals(input.getOperations(), keyMaterial.getKeyOps()); + } + + private void assertIdsEqual(final VersionedKeyEntityId input, final KeyBackupListItem actual) { + Assertions.assertEquals(input.vault(), actual.getVaultBaseUri()); + Assertions.assertEquals(input.id(), actual.getId()); + Assertions.assertEquals(input.version(), actual.getVersion()); + } +} diff --git a/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/model/v7_2/secret/SecretEntityToV72BackupConverterTest.java b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/model/v7_2/secret/SecretEntityToV72BackupConverterTest.java new file mode 100644 index 00000000..d280f8ce --- /dev/null +++ b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/model/v7_2/secret/SecretEntityToV72BackupConverterTest.java @@ -0,0 +1,108 @@ +package com.github.nagyesta.lowkeyvault.model.v7_2.secret; + +import com.github.nagyesta.lowkeyvault.mapper.v7_2.secret.SecretEntityToV72PropertiesModelConverter; +import com.github.nagyesta.lowkeyvault.service.secret.ReadOnlyKeyVaultSecretEntity; +import com.github.nagyesta.lowkeyvault.service.secret.impl.KeyVaultSecretEntity; +import com.github.nagyesta.lowkeyvault.service.vault.VaultFake; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.util.MimeTypeUtils; + +import java.util.Collections; +import java.util.Map; + +import static com.github.nagyesta.lowkeyvault.TestConstants.*; +import static com.github.nagyesta.lowkeyvault.TestConstantsSecrets.VERSIONED_SECRET_ENTITY_ID_1_VERSION_1; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +class SecretEntityToV72BackupConverterTest { + + private static final SecretPropertiesModel SECRET_PROPERTIES_MODEL = new SecretPropertiesModel(); + @Mock + private VaultFake vaultFake; + @Mock + private SecretEntityToV72PropertiesModelConverter propertiesModelConverter; + @InjectMocks + private SecretEntityToV72BackupConverter underTest; + private AutoCloseable openMocks; + + @BeforeEach + void setUp() { + openMocks = MockitoAnnotations.openMocks(this); + when(propertiesModelConverter.convert(any(ReadOnlyKeyVaultSecretEntity.class))).thenReturn(SECRET_PROPERTIES_MODEL); + } + + @AfterEach + void tearDown() throws Exception { + openMocks.close(); + } + + @SuppressWarnings("ConstantConditions") + @Test + void testConstructorShouldThrowExceptionWhenCalledWithNull() { + //given + + //when + Assertions.assertThrows(IllegalArgumentException.class, () -> new SecretEntityToV72BackupConverter(null)); + + //then + exception + } + + @Test + void testConvertShouldConvertPopulatedFieldsWhenCalledWithMinimalInput() { + //given + final String value = LOWKEY_VAULT; + final ReadOnlyKeyVaultSecretEntity input = new KeyVaultSecretEntity( + VERSIONED_SECRET_ENTITY_ID_1_VERSION_1, vaultFake, value, null); + + //when + final SecretBackupListItem actual = underTest.convert(input); + + //then + Assertions.assertNotNull(actual); + Assertions.assertEquals(value, actual.getValue()); + Assertions.assertNull(actual.getContentType()); + Assertions.assertSame(SECRET_PROPERTIES_MODEL, actual.getAttributes()); + Assertions.assertEquals(Collections.emptyMap(), actual.getTags()); + Assertions.assertFalse(actual.isManaged()); + Assertions.assertEquals(input.getId().vault(), actual.getVaultBaseUri()); + Assertions.assertEquals(input.getId().id(), actual.getId()); + Assertions.assertEquals(input.getId().version(), actual.getVersion()); + verify(propertiesModelConverter).convert(any(ReadOnlyKeyVaultSecretEntity.class)); + verifyNoMoreInteractions(propertiesModelConverter); + } + + @Test + void testConvertShouldConvertAllFieldsWhenCalledWithFullyPopulatedInput() { + //given + final String contentType = MimeTypeUtils.TEXT_PLAIN_VALUE; + final String value = LOWKEY_VAULT; + final Map tagMap = Map.of(KEY_1, VALUE_1); + final KeyVaultSecretEntity input = new KeyVaultSecretEntity( + VERSIONED_SECRET_ENTITY_ID_1_VERSION_1, vaultFake, value, contentType); + input.setTags(tagMap); + input.setManaged(true); + + //when + final SecretBackupListItem actual = underTest.convert(input); + + //then + Assertions.assertNotNull(actual); + Assertions.assertEquals(value, actual.getValue()); + Assertions.assertEquals(contentType, actual.getContentType()); + Assertions.assertSame(SECRET_PROPERTIES_MODEL, actual.getAttributes()); + Assertions.assertEquals(tagMap, actual.getTags()); + Assertions.assertTrue(actual.isManaged()); + Assertions.assertEquals(input.getId().vault(), actual.getVaultBaseUri()); + Assertions.assertEquals(input.getId().id(), actual.getId()); + Assertions.assertEquals(input.getId().version(), actual.getVersion()); + verify(propertiesModelConverter).convert(any(ReadOnlyKeyVaultSecretEntity.class)); + verifyNoMoreInteractions(propertiesModelConverter); + } +} diff --git a/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/service/secret/impl/SecretVaultFakeImplTest.java b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/service/secret/impl/SecretVaultFakeImplTest.java index 6762b274..1c355799 100644 --- a/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/service/secret/impl/SecretVaultFakeImplTest.java +++ b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/service/secret/impl/SecretVaultFakeImplTest.java @@ -38,7 +38,7 @@ void testCreateSecretVersionShouldThrowExceptionWhenCalledWithNullName() { //when Assertions.assertThrows(IllegalArgumentException.class, - () -> underTest.createSecretVersion(null, LOWKEY_VAULT, null)); + () -> underTest.createSecretVersion((String) null, LOWKEY_VAULT, null)); //then + exception } @@ -58,6 +58,36 @@ void testCreateSecretVersionShouldThrowExceptionWhenCalledWithNullValue() { //then + exception } + @SuppressWarnings("ConstantConditions") + @Test + void testCreateSecretVersionUsingVersionedIdShouldThrowExceptionWhenCalledWithNullValue() { + //given + final VaultFake vaultFake = new VaultFakeImpl(HTTPS_LOCALHOST_8443); + final SecretVaultFakeImpl underTest = + new SecretVaultFakeImpl(vaultFake, vaultFake.getRecoveryLevel(), vaultFake.getRecoverableDays()); + + //when + Assertions.assertThrows(IllegalArgumentException.class, + () -> underTest.createSecretVersion(VERSIONED_SECRET_ENTITY_ID_1_VERSION_1, null, null)); + + //then + exception + } + + @SuppressWarnings("ConstantConditions") + @Test + void testCreateSecretVersionUsingVersionedIdShouldThrowExceptionWhenCalledWithNullEntityId() { + //given + final VaultFake vaultFake = new VaultFakeImpl(HTTPS_LOCALHOST_8443); + final SecretVaultFakeImpl underTest = + new SecretVaultFakeImpl(vaultFake, vaultFake.getRecoveryLevel(), vaultFake.getRecoverableDays()); + + //when + Assertions.assertThrows(IllegalArgumentException.class, + () -> underTest.createSecretVersion((VersionedSecretEntityId) null, LOWKEY_VAULT, null)); + + //then + exception + } + @Test void testCreateSecretVersionShouldCreateNewEntityWhenCalledWithValidInput() { //given diff --git a/lowkey-vault-docker/src/test/java/com/github/nagyesta/lowkeyvault/context/CommonTestContext.java b/lowkey-vault-docker/src/test/java/com/github/nagyesta/lowkeyvault/context/CommonTestContext.java index 7e17405c..4de518dc 100644 --- a/lowkey-vault-docker/src/test/java/com/github/nagyesta/lowkeyvault/context/CommonTestContext.java +++ b/lowkey-vault-docker/src/test/java/com/github/nagyesta/lowkeyvault/context/CommonTestContext.java @@ -4,6 +4,7 @@ import java.time.OffsetDateTime; import java.time.ZoneOffset; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -23,6 +24,7 @@ public abstract class CommonTestContext { private P updateProperties; private List listedIds; private List deletedRecoveryIds; + private Map backups = new HashMap(); public CommonTestContext(final ApacheHttpClientProvider provider) { this.provider = provider; @@ -43,12 +45,12 @@ public synchronized C getClient() { return client; } - protected abstract C providerToClient(ApacheHttpClientProvider provider); - public void setClient(final C client) { this.client = client; } + protected abstract C providerToClient(ApacheHttpClientProvider provider); + public Map> getCreatedEntities() { return createdEntities; } @@ -110,4 +112,12 @@ public P getUpdateProperties() { public void setUpdateProperties(final P updateProperties) { this.updateProperties = updateProperties; } + + public void setBackupBytes(final String name, final byte[] bytes) { + backups.put(name, bytes); + } + + public byte[] getBackupBytes(final String name) { + return backups.get(name); + } } diff --git a/lowkey-vault-docker/src/test/java/com/github/nagyesta/lowkeyvault/hook/MissionOutlineDefinition.java b/lowkey-vault-docker/src/test/java/com/github/nagyesta/lowkeyvault/hook/MissionOutlineDefinition.java index 8f72fa3e..e55d67cc 100644 --- a/lowkey-vault-docker/src/test/java/com/github/nagyesta/lowkeyvault/hook/MissionOutlineDefinition.java +++ b/lowkey-vault-docker/src/test/java/com/github/nagyesta/lowkeyvault/hook/MissionOutlineDefinition.java @@ -36,15 +36,16 @@ protected Map> defineOutline() { .build(); ops.registerHealthCheck(featurePercentage); - Stream.of("Create", "Get", "Delete", "List", "Update", "ListDeleted", "Recover", "Purge").forEach(subtype -> { - final MissionHealthCheckMatcher subTypeMatcher = matcher().dependencyWith(type + subtype) - .extractor(extractor).build(); - final MissionHealthCheckEvaluator subFeaturePercentage = percentageBasedEvaluator(subTypeMatcher) - .abortThreshold(ABORT_THRESHOLD) - .burnInTestCount(BURN_IN_TEST_COUNT) - .build(); - ops.registerHealthCheck(subFeaturePercentage); - }); + Stream.of("Create", "Get", "Delete", "List", "Update", "ListDeleted", "Recover", "Purge", "Backup", "Restore") + .forEach(subtype -> { + final MissionHealthCheckMatcher subTypeMatcher = matcher().dependencyWith(type + subtype) + .extractor(extractor).build(); + final MissionHealthCheckEvaluator subFeaturePercentage = percentageBasedEvaluator(subTypeMatcher) + .abortThreshold(ABORT_THRESHOLD) + .burnInTestCount(BURN_IN_TEST_COUNT) + .build(); + ops.registerHealthCheck(subFeaturePercentage); + }); }); Stream.of("CreateVault", "KeyImport", "KeyEncrypt", "KeySign", "RSA", "EC", "OCT").forEach(tag -> { diff --git a/lowkey-vault-docker/src/test/java/com/github/nagyesta/lowkeyvault/steps/KeysStepDefs.java b/lowkey-vault-docker/src/test/java/com/github/nagyesta/lowkeyvault/steps/KeysStepDefs.java index b054de0a..42210bbc 100644 --- a/lowkey-vault-docker/src/test/java/com/github/nagyesta/lowkeyvault/steps/KeysStepDefs.java +++ b/lowkey-vault-docker/src/test/java/com/github/nagyesta/lowkeyvault/steps/KeysStepDefs.java @@ -387,6 +387,19 @@ public void theRsaSignValueIsVerifiedUsingOriginalPublicKeyWithAlgorithm(final b context.setVerifyResult(result); } + @And("the key named {name} is backed up") + public void theKeyNamedNameIsBackedUp(final String name) { + final byte[] bytes = context.getClient().backupKey(name); + context.setBackupBytes(name, bytes); + } + + @And("the key named {name} is restored") + public void theKeyNamedNameIsRestored(final String name) { + final byte[] bytes = context.getBackupBytes(name); + final KeyVaultKey key = context.getClient().restoreKeyBackup(bytes); + context.addFetchedKey(name, key); + } + private byte[] hash(final byte[] text, final String algorithm) { try { final MessageDigest md = MessageDigest.getInstance("SHA-" + algorithm.substring(2, 5)); diff --git a/lowkey-vault-docker/src/test/java/com/github/nagyesta/lowkeyvault/steps/KeysStepDefsAssertions.java b/lowkey-vault-docker/src/test/java/com/github/nagyesta/lowkeyvault/steps/KeysStepDefsAssertions.java index f7bee3c1..6356a282 100644 --- a/lowkey-vault-docker/src/test/java/com/github/nagyesta/lowkeyvault/steps/KeysStepDefsAssertions.java +++ b/lowkey-vault-docker/src/test/java/com/github/nagyesta/lowkeyvault/steps/KeysStepDefsAssertions.java @@ -162,4 +162,10 @@ public void theSignatureMatches() { public void theListedDeletedKeysAreEmpty() { assertEquals(Collections.emptyList(), context.getListedIds()); } + + @And("the key named {name} matches the previous backup") + public void theKeyNamedNameMatchesThePreviousBackup(final String name) { + final byte[] bytes = context.getClient().backupKey(name); + assertEquals(context.getBackupBytes(name), bytes); + } } diff --git a/lowkey-vault-docker/src/test/java/com/github/nagyesta/lowkeyvault/steps/SecretsStepDefs.java b/lowkey-vault-docker/src/test/java/com/github/nagyesta/lowkeyvault/steps/SecretsStepDefs.java index ee22b2a3..98967fa8 100644 --- a/lowkey-vault-docker/src/test/java/com/github/nagyesta/lowkeyvault/steps/SecretsStepDefs.java +++ b/lowkey-vault-docker/src/test/java/com/github/nagyesta/lowkeyvault/steps/SecretsStepDefs.java @@ -31,7 +31,7 @@ public class SecretsStepDefs extends CommonAssertions { public void theSecretClientIsCreatedWithVaultNameSelected(final String vaultName) { final String vaultAuthority = vaultName + ".localhost:8443"; final String vaultUrl = "https://" + vaultAuthority; - final AuthorityOverrideFunction overrideFunction = new AuthorityOverrideFunction(vaultAuthority,CONTAINER_AUTHORITY); + final AuthorityOverrideFunction overrideFunction = new AuthorityOverrideFunction(vaultAuthority, CONTAINER_AUTHORITY); context.setProvider(new ApacheHttpClientProvider(vaultUrl, overrideFunction)); } @@ -191,4 +191,17 @@ public void theUpdateRequestIsSent() { final SecretProperties properties = context.getClient().updateSecretProperties(context.getUpdateProperties()); fetchLatestSecretVersion(properties.getName()); } + + @And("the secret named {name} is backed up") + public void theSecretNamedNameIsBackedUp(final String name) { + final byte[] bytes = context.getClient().backupSecret(name); + context.setBackupBytes(name, bytes); + } + + @And("the secret named {name} is restored") + public void theSecretNamedNameIsRestored(final String name) { + final byte[] bytes = context.getBackupBytes(name); + final KeyVaultSecret secret = context.getClient().restoreSecretBackup(bytes); + context.addFetchedSecret(name, secret); + } } diff --git a/lowkey-vault-docker/src/test/resources/com/github/nagyesta/lowkeyvault/keys/BackupAndRestoreKeys.feature b/lowkey-vault-docker/src/test/resources/com/github/nagyesta/lowkeyvault/keys/BackupAndRestoreKeys.feature new file mode 100644 index 00000000..5731585b --- /dev/null +++ b/lowkey-vault-docker/src/test/resources/com/github/nagyesta/lowkeyvault/keys/BackupAndRestoreKeys.feature @@ -0,0 +1,79 @@ +Feature: Key backup and restore + + @Key @KeyImport @KeySign @KeyBackup @KeyRestore @RSA + Scenario Outline: RSA_BACKUP_01 An RSA key is imported, backed up, vault is recreated then after restore, the key is verified + Given a vault is created with name keys-backup- + And a key client is created with the vault named keys-backup- + And an RSA key is imported with as name and bits of key size without HSM + And the key named is backed up + And the vault named keys-backup- is deleted + And the vault named keys-backup- is purged + And a vault is created with name keys-backup- + When the key named is restored + Then the created key is used to sign with + And the signed value is not + And the RSA signature of is verified using the original public key with + And the signature matches + And the key named matches the previous backup + + Examples: + | keyName | keySize | algorithm | clearText | + | backupRsaKey-1 | 2048 | PS256 | The quick brown fox jumps over the lazy dog. | + | backupRsaKey-2 | 2048 | PS384 | | + | backupRsaKey-3 | 2048 | PS512 | The quick brown fox jumps over the lazy dog. | + | backupRsaKey-4 | 4096 | RS256 | The quick brown fox jumps over the lazy dog. | + | backupRsaKey-5 | 4096 | RS384 | | + | backupRsaKey-6 | 4096 | RS512 | The quick brown fox jumps over the lazy dog. | + + @Key @KeyImport @KeySign @KeyBackup @KeyRestore @EC + Scenario Outline: EC_BACKUP_01 An EC key is imported, backed up, vault is recreated then after restore, the key is verified + Given a vault is created with name keys-backup- + And a key client is created with the vault named keys-backup- + And an EC key is imported with as name and curve without HSM + And the key named is backed up + And the vault named keys-backup- is deleted + And the vault named keys-backup- is purged + And a vault is created with name keys-backup- + When the key named is restored + Then the created key is used to sign with + And the signed value is not + And the EC signature of is verified using the original public key with + And the signature matches + And the key named matches the previous backup + + Examples: + | keyName | curveName | algorithm | clearText | + | backupEc-1 | P-256 | ES256 | The quick brown fox jumps over the lazy dog. | + | backupEc-2 | P-256K | ES256K | The quick brown fox jumps over the lazy dog. | + | backupEc-3 | P-384 | ES384 | The quick brown fox jumps over the lazy dog. | + | backupEc-4 | P-521 | ES512 | The quick brown fox jumps over the lazy dog. | + | backupEc-5 | P-256 | ES256 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do. | + | backupEc-6 | P-256K | ES256K | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do. | + | backupEc-7 | P-384 | ES384 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do. | + | backupEc-8 | P-521 | ES512 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do. | + + + @Key @KeyImport @KeyEncrypt @KeyBackup @KeyRestore @OCT + Scenario Outline: OCT_BACKUP_01 An OCT key is imported, backed up, vault is recreated then after restore, the key is verified + Given a vault is created with name keys-backup- + And a key client is created with the vault named keys-backup- + And an OCT key is imported with as name and bits of key size with HSM + And the key named is backed up + And the vault named keys-backup- is deleted + And the vault named keys-backup- is purged + And a vault is created with name keys-backup- + When the key named is restored + Then the created key is used to encrypt with + And the encrypted value is not + And the encrypted value is decrypted using the original OCT key using + And the decrypted value is + And the key named matches the previous backup + + Examples: + | keyName | keySize | algorithm | clearText | + | backupOct-1 | 128 | A128CBCPAD | The quick brown fox jumps over the lazy dog. | + | backupOct-2 | 192 | A192CBCPAD | The quick brown fox jumps over the lazy dog. | + | backupOct-3 | 256 | A256CBCPAD | The quick brown fox jumps over the lazy dog. | + | backupOct-4 | 128 | A128CBC | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do. | + | backupOct-5 | 192 | A192CBC | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do. | + | backupOct-6 | 256 | A256CBC | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do. | diff --git a/lowkey-vault-docker/src/test/resources/com/github/nagyesta/lowkeyvault/secrets/BackupAndRestoreSecrets.feature b/lowkey-vault-docker/src/test/resources/com/github/nagyesta/lowkeyvault/secrets/BackupAndRestoreSecrets.feature new file mode 100644 index 00000000..a8baf309 --- /dev/null +++ b/lowkey-vault-docker/src/test/resources/com/github/nagyesta/lowkeyvault/secrets/BackupAndRestoreSecrets.feature @@ -0,0 +1,23 @@ +Feature: Secret backup and restore + + @Secret @SecretCreate @DeleteVault @SecretBackup @SecretRestore @CreateVault + Scenario Outline: SECRET_BACKUP_01 Secrets are created and backed up then vault is deleted and recreated to restore secret + Given a vault is created with name secrets-backup- + And a secret client is created with the vault named secrets-backup- + And secrets with - prefix are created valued abc123 + And the secret named - is backed up + And the vault named secrets-backup- is deleted + And the vault named secrets-backup- is purged + And a vault is created with name secrets-backup- + When the secret named - is restored + Then the last secret version of - is fetched without providing a version + And the created secret exists with value: abc123 + + Examples: + | count | secretName | + | 1 | backupSecret | + | 2 | backup-secret-name | + | 3 | backupSecret | + | 5 | backup-secret-name | + | 25 | backupSecret | + | 42 | backup-secret-name |