Skip to content

Commit

Permalink
7.3 API - Key rotation logic (#188)
Browse files Browse the repository at this point in the history
- Adds support for key rotation policies internally
- Adds converters to and from key rotation policy entries and models
- Adds new endpoint to support ad-hoc key rotation requests
- [Breaking] Includes rotation policy in backups (and uses them in restore)
- Adds new test cases
- Updates readme

Resolves #181
{minor}

Signed-off-by: Esta Nagy <nagyesta@gmail.com>
  • Loading branch information
nagyesta authored Jun 6, 2022
1 parent c1d0353 commit 3e18dba
Show file tree
Hide file tree
Showing 116 changed files with 3,947 additions and 564 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ Lowkey Vault is far from supporting all Azure Key Vault features. The list suppo
- ```ES512```
- Backup and restore keys
- Get random bytes
- Rotate keys (manually)

### Secrets

Expand Down
4 changes: 3 additions & 1 deletion config/checkstyle/checkstyle.xml
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,9 @@
<!-- </module>-->
<module name="IllegalInstantiation"/>
<module name="InnerAssignment"/>
<module name="MagicNumber"/>
<module name="MagicNumber">
<property name="ignoreAnnotation" value="true" />
</module>
<module name="MissingSwitchDefault"/>
<module name="SimplifyBooleanExpression"/>
<module name="SimplifyBooleanReturn"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
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.model.v7_2.key.BackupListContainer;
import com.github.nagyesta.lowkeyvault.service.EntityId;
import com.github.nagyesta.lowkeyvault.service.common.BaseVaultEntity;
import com.github.nagyesta.lowkeyvault.service.common.BaseVaultFake;
Expand Down Expand Up @@ -40,8 +41,8 @@
* @param <S> The fake type holding the entities.
*/
public abstract class BaseBackupRestoreController<K extends EntityId, V extends K, E extends BaseVaultEntity<V>, M, DM extends M,
P extends BasePropertiesModel, BLI extends BaseBackupListItem<P>, BL extends List<BLI>, B extends BaseBackupModel<P, BLI, BL>,
BC extends BackupConverter<V, E, P, BLI>, MC extends RecoveryAwareConverter<E, M, DM>,
P extends BasePropertiesModel, BLI extends BaseBackupListItem<P>, BL extends BackupListContainer<BLI>,
B extends BaseBackupModel<P, BLI, BL>, BC extends BackupConverter<V, E, P, BLI>, MC extends RecoveryAwareConverter<E, M, DM>,
S extends BaseVaultFake<K, V, E>> extends BaseEntityReadController<K, V, E, S> {

private final MC modelConverter;
Expand All @@ -62,7 +63,7 @@ protected M restoreEntity(final B backupModel) {
final String id = getSingleEntityName(backupModel);
final K entityId = entityId(baseUri, id);
assertNameDoesNotExistYet(vault, entityId);
backupModel.getValue().forEach(entityVersion -> {
backupModel.getValue().getVersions().forEach(entityVersion -> {
final V versionedEntityId = versionedEntityId(baseUri, id, entityVersion.getVersion());
restoreVersion(vault, versionedEntityId, entityVersion);
});
Expand Down Expand Up @@ -98,11 +99,29 @@ protected B backupEntity(final K entityId) {

protected abstract BL getBackupList();

protected String getSingleEntityName(final B backupModel) {
final List<String> entityNames = backupModel.getValue().getVersions().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);
}

protected URI getSingleBaseUri(final B backupModel) {
final List<URI> uris = backupModel.getValue().getVersions().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);
}

private B wrapBackup(final List<BLI> list) {
final BL listModel = Optional.ofNullable(list)
.map(l -> {
final BL backupList = getBackupList();
backupList.addAll(l);
backupList.setVersions(l);
return backupList;
})
.orElse(null);
Expand All @@ -118,22 +137,4 @@ private void assertNameDoesNotExistYet(final S vault, final K entityId) {
"Vault already contains deleted entity with name: " + entityId.id());
}

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

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

}
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
package com.github.nagyesta.lowkeyvault.controller.common;

import com.github.nagyesta.lowkeyvault.mapper.v7_2.key.KeyEntityToV72BackupConverter;
import com.github.nagyesta.lowkeyvault.mapper.common.BackupConverter;
import com.github.nagyesta.lowkeyvault.mapper.v7_2.key.KeyEntityToV72ModelConverter;
import com.github.nagyesta.lowkeyvault.model.v7_2.key.*;
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.model.v7_2.key.BackupListContainer;
import com.github.nagyesta.lowkeyvault.model.v7_2.key.DeletedKeyVaultKeyModel;
import com.github.nagyesta.lowkeyvault.model.v7_2.key.KeyPropertiesModel;
import com.github.nagyesta.lowkeyvault.model.v7_2.key.KeyVaultKeyModel;
import com.github.nagyesta.lowkeyvault.model.v7_2.key.request.JsonWebKeyImportRequest;
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;
Expand All @@ -20,19 +26,30 @@
import java.util.Collections;
import java.util.Objects;

/**
* Common logic of backup and restore controllers across the different API versions.
*
* @param <BLI> The type of the list item representing one entity version in the backup model.
* @param <BL> The wrapper type of the list in the backup model.
* @param <B> The type of the backup model.
* @param <BC> The converter, converting entities to list items of the backup models.
*/
@Slf4j
public abstract class CommonKeyBackupRestoreController extends BaseBackupRestoreController<KeyEntityId, VersionedKeyEntityId,
ReadOnlyKeyVaultKeyEntity, KeyVaultKeyModel, DeletedKeyVaultKeyModel, KeyPropertiesModel, KeyBackupListItem, KeyBackupList,
KeyBackupModel, KeyEntityToV72BackupConverter, KeyEntityToV72ModelConverter, KeyVaultFake> {
public abstract class CommonKeyBackupRestoreController<BLI extends BaseBackupListItem<KeyPropertiesModel>,
BL extends BackupListContainer<BLI>, B extends BaseBackupModel<KeyPropertiesModel, BLI, BL>,
BC extends BackupConverter<VersionedKeyEntityId, ReadOnlyKeyVaultKeyEntity, KeyPropertiesModel, BLI>>
extends BaseBackupRestoreController<KeyEntityId, VersionedKeyEntityId,
ReadOnlyKeyVaultKeyEntity, KeyVaultKeyModel, DeletedKeyVaultKeyModel, KeyPropertiesModel, BLI, BL,
B, BC, KeyEntityToV72ModelConverter, KeyVaultFake> {

protected CommonKeyBackupRestoreController(
@NonNull final KeyEntityToV72ModelConverter modelConverter,
@NonNull final KeyEntityToV72BackupConverter backupConverter,
@NonNull final BC backupConverter,
@NonNull final VaultService vaultService) {
super(modelConverter, backupConverter, vaultService, VaultFake::keyVaultFake);
}

public ResponseEntity<KeyBackupModel> backup(
public ResponseEntity<B> backup(
@Valid @Pattern(regexp = NAME_PATTERN) final String keyName,
final URI baseUri) {
log.info("Received request to {} backup key: {} using API version: {}",
Expand All @@ -42,32 +59,22 @@ public ResponseEntity<KeyBackupModel> backup(

public ResponseEntity<KeyVaultKeyModel> restore(
final URI baseUri,
@Valid final KeyBackupModel keyBackupModel) {
@Valid final B keyBackupModel) {
log.info("Received request to {} restore key: {} using API version: {}",
baseUri.toString(), keyBackupModel.getValue().get(0).getId(), apiVersion());
baseUri.toString(), keyBackupModel.getValue().getVersions().get(0).getId(), apiVersion());
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());
@NonNull final BLI entityVersion) {
vault.importKeyVersion(versionedEntityId, getKeyMaterial(entityVersion));
final KeyVaultKeyEntity<?, ?> entity = vault.getEntities().getEntity(versionedEntityId, KeyVaultKeyEntity.class);
entity.setOperations(Objects.requireNonNullElse(entityVersion.getKeyMaterial().getKeyOps(), Collections.emptyList()));
entity.setOperations(Objects.requireNonNullElse(getKeyMaterial(entityVersion).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);
Expand All @@ -77,4 +84,6 @@ protected VersionedKeyEntityId versionedEntityId(final URI baseUri, final String
protected KeyEntityId entityId(final URI baseUri, final String name) {
return new KeyEntityId(baseUri, name);
}

protected abstract JsonWebKeyImportRequest getKeyMaterial(BLI entityVersion);
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public ResponseEntity<KeyVaultSecretModel> restore(
final URI baseUri,
@Valid final SecretBackupModel secretBackupModel) {
log.info("Received request to {} restore secret: {} using API version: {}",
baseUri.toString(), secretBackupModel.getValue().get(0).getId(), apiVersion());
baseUri.toString(), secretBackupModel.getValue().getVersions().get(0).getId(), apiVersion());
return ResponseEntity.ok(restoreEntity(secretBackupModel));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
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.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.KeyVaultKeyModel;
import com.github.nagyesta.lowkeyvault.model.v7_2.key.request.JsonWebKeyImportRequest;
import com.github.nagyesta.lowkeyvault.service.vault.VaultService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -26,7 +29,8 @@
@RestController
@Validated
@Component("KeyBackupRestoreControllerV72")
public class KeyBackupRestoreController extends CommonKeyBackupRestoreController {
public class KeyBackupRestoreController
extends CommonKeyBackupRestoreController<KeyBackupListItem, KeyBackupList, KeyBackupModel, KeyEntityToV72BackupConverter> {

@Autowired
protected KeyBackupRestoreController(final KeyEntityToV72ModelConverter modelConverter,
Expand Down Expand Up @@ -54,6 +58,21 @@ public ResponseEntity<KeyVaultKeyModel> restore(@RequestAttribute(name = ApiCons
return super.restore(baseUri, keyBackupModel);
}

@Override
protected JsonWebKeyImportRequest getKeyMaterial(final KeyBackupListItem entityVersion) {
return entityVersion.getKeyMaterial();
}

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

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

@Override
protected String apiVersion() {
return V_7_2;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,31 @@
import com.github.nagyesta.lowkeyvault.controller.common.CommonKeyBackupRestoreController;
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.mapper.v7_3.key.KeyRotationPolicyToV73ModelConverter;
import com.github.nagyesta.lowkeyvault.mapper.v7_3.key.KeyRotationPolicyV73ModelToEntityConverter;
import com.github.nagyesta.lowkeyvault.model.common.ApiConstants;
import com.github.nagyesta.lowkeyvault.model.v7_2.key.KeyBackupModel;
import com.github.nagyesta.lowkeyvault.model.v7_2.key.KeyBackupListItem;
import com.github.nagyesta.lowkeyvault.model.v7_2.key.KeyVaultKeyModel;
import com.github.nagyesta.lowkeyvault.model.v7_2.key.request.JsonWebKeyImportRequest;
import com.github.nagyesta.lowkeyvault.model.v7_3.key.KeyBackupList;
import com.github.nagyesta.lowkeyvault.model.v7_3.key.KeyBackupModel;
import com.github.nagyesta.lowkeyvault.model.v7_3.key.KeyRotationPolicyModel;
import com.github.nagyesta.lowkeyvault.service.key.KeyVaultFake;
import com.github.nagyesta.lowkeyvault.service.key.ReadOnlyRotationPolicy;
import com.github.nagyesta.lowkeyvault.service.key.id.KeyEntityId;
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.stereotype.Component;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

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

import static com.github.nagyesta.lowkeyvault.model.common.ApiConstants.API_VERSION_7_3;
import static com.github.nagyesta.lowkeyvault.model.common.ApiConstants.V_7_3;
Expand All @@ -26,13 +37,21 @@
@RestController
@Validated
@Component("KeyBackupRestoreControllerV73")
public class KeyBackupRestoreController extends CommonKeyBackupRestoreController {
public class KeyBackupRestoreController
extends CommonKeyBackupRestoreController<KeyBackupListItem, KeyBackupList, KeyBackupModel, KeyEntityToV72BackupConverter> {

private final KeyRotationPolicyToV73ModelConverter keyRotationPolicyToV73ModelConverter;
private final KeyRotationPolicyV73ModelToEntityConverter rotationV73ModelToEntityConverter;

@Autowired
protected KeyBackupRestoreController(final KeyEntityToV72ModelConverter modelConverter,
final KeyEntityToV72BackupConverter backupConverter,
final VaultService vaultService) {
protected KeyBackupRestoreController(@NonNull final KeyEntityToV72ModelConverter modelConverter,
@NonNull final KeyEntityToV72BackupConverter backupConverter,
@NonNull final VaultService vaultService,
@NonNull final KeyRotationPolicyToV73ModelConverter keyRotationPolicyToV73ModelConverter,
@NonNull final KeyRotationPolicyV73ModelToEntityConverter rotationV73ModelToEntityConverter) {
super(modelConverter, backupConverter, vaultService);
this.keyRotationPolicyToV73ModelConverter = keyRotationPolicyToV73ModelConverter;
this.rotationV73ModelToEntityConverter = rotationV73ModelToEntityConverter;
}

@Override
Expand All @@ -41,7 +60,6 @@ protected KeyBackupRestoreController(final KeyEntityToV72ModelConverter modelCon
produces = APPLICATION_JSON_VALUE)
public ResponseEntity<KeyBackupModel> backup(@PathVariable @Valid @Pattern(regexp = NAME_PATTERN) final String keyName,
@RequestAttribute(name = ApiConstants.REQUEST_BASE_URI) final URI baseUri) {
//handle differences of rotation policy
return super.backup(keyName, baseUri);
}

Expand All @@ -55,8 +73,46 @@ public ResponseEntity<KeyVaultKeyModel> restore(@RequestAttribute(name = ApiCons
return super.restore(baseUri, keyBackupModel);
}

@Override
protected KeyBackupModel backupEntity(final KeyEntityId entityId) {
final KeyBackupModel keyBackupModel = super.backupEntity(entityId);
final KeyBackupList value = keyBackupModel.getValue();
final ReadOnlyRotationPolicy rotationPolicy = getVaultByUri(entityId.vault()).rotationPolicy(entityId);
value.setKeyRotationPolicy(keyRotationPolicyToV73ModelConverter.convert(rotationPolicy));
return keyBackupModel;
}

@Override
protected KeyVaultKeyModel restoreEntity(final KeyBackupModel backupModel) {
final KeyVaultKeyModel keyVaultKeyModel = super.restoreEntity(backupModel);
final URI baseUri = getSingleBaseUri(backupModel);
final String entityName = getSingleEntityName(backupModel);
final KeyEntityId keyEntityId = entityId(baseUri, entityName);
final KeyVaultFake vaultByUri = getVaultByUri(baseUri);
final KeyRotationPolicyModel keyRotationPolicy = backupModel.getValue().getKeyRotationPolicy();
Optional.ofNullable(keyRotationPolicy)
.map(r -> rotationV73ModelToEntityConverter.convert(keyEntityId, r))
.ifPresent(vaultByUri::setRotationPolicy);
return keyVaultKeyModel;
}

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

@Override
protected JsonWebKeyImportRequest getKeyMaterial(final KeyBackupListItem entityVersion) {
return entityVersion.getKeyMaterial();
}

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

@Override
protected KeyBackupModel getBackupModel() {
return new KeyBackupModel();
}
}
Loading

0 comments on commit 3e18dba

Please sign in to comment.