Add support for RevisionRepository.
Vault repositories can now implement RevisionRepository to access older secret revisions. See gh-593
This commit is contained in:
@@ -22,6 +22,7 @@ import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.vault.support.VaultResponse;
|
||||
import org.springframework.vault.support.Versioned;
|
||||
|
||||
/**
|
||||
* Vault database exchange object containing data before/after it's exchanged with Vault.
|
||||
|
||||
@@ -268,6 +268,14 @@ public class VaultKeyValueAdapter extends AbstractKeyValueAdapter {
|
||||
|
||||
}
|
||||
|
||||
public VaultConverter getConverter() {
|
||||
return this.vaultConverter;
|
||||
}
|
||||
|
||||
public VaultOperations getVaultOperations() {
|
||||
return this.vaultOperations;
|
||||
}
|
||||
|
||||
static abstract class VaultKeyValueKeyspaceAccessor {
|
||||
|
||||
private final KeyValueDelegate.MountInfo mountInfo;
|
||||
|
||||
@@ -28,6 +28,8 @@ import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.vault.core.VaultOperations;
|
||||
import org.springframework.vault.repository.convert.VaultConverter;
|
||||
import org.springframework.vault.repository.mapping.VaultMappingContext;
|
||||
|
||||
/**
|
||||
@@ -177,4 +179,12 @@ public class VaultKeyValueTemplate extends KeyValueTemplate {
|
||||
}
|
||||
}
|
||||
|
||||
public VaultConverter getConverter() {
|
||||
return execute(adapter -> ((VaultKeyValueAdapter) adapter).getConverter());
|
||||
}
|
||||
|
||||
public VaultOperations getVaultOperations() {
|
||||
return execute(adapter -> ((VaultKeyValueAdapter) adapter).getVaultOperations());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,10 +19,15 @@ import org.springframework.data.keyvalue.core.KeyValueOperations;
|
||||
import org.springframework.data.keyvalue.repository.query.KeyValuePartTreeQuery;
|
||||
import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactory;
|
||||
import org.springframework.data.repository.core.EntityInformation;
|
||||
import org.springframework.data.repository.core.RepositoryMetadata;
|
||||
import org.springframework.data.repository.core.support.RepositoryComposition;
|
||||
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
|
||||
import org.springframework.data.repository.core.support.RepositoryFragment;
|
||||
import org.springframework.data.repository.history.RevisionRepository;
|
||||
import org.springframework.data.repository.query.RepositoryQuery;
|
||||
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
|
||||
import org.springframework.vault.repository.core.MappingVaultEntityInformation;
|
||||
import org.springframework.vault.repository.core.VaultKeyValueTemplate;
|
||||
import org.springframework.vault.repository.mapping.VaultPersistentEntity;
|
||||
import org.springframework.vault.repository.query.VaultQueryCreator;
|
||||
|
||||
@@ -54,12 +59,35 @@ public class VaultRepositoryFactory extends KeyValueRepositoryFactory {
|
||||
this.operations = keyValueOperations;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RepositoryComposition.RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata,
|
||||
KeyValueOperations operations) {
|
||||
|
||||
RepositoryComposition.RepositoryFragments fragments = super.getRepositoryFragments(metadata, operations);
|
||||
|
||||
if (RevisionRepository.class.isAssignableFrom(metadata.getRepositoryInterface())
|
||||
&& operations instanceof VaultKeyValueTemplate) {
|
||||
|
||||
VaultKeyValueTemplate template = (VaultKeyValueTemplate) operations;
|
||||
|
||||
VaultPersistentEntity<?> entity = (VaultPersistentEntity<?>) this.operations.getMappingContext()
|
||||
.getRequiredPersistentEntity(metadata.getDomainType());
|
||||
EntityInformation<?, String> entityInformation = getEntityInformation(metadata.getDomainType());
|
||||
VaultRevisionRepository<?> repository = new VaultRevisionRepository<>(entityInformation,
|
||||
entity.getKeySpace(), template);
|
||||
|
||||
return fragments.append(RepositoryFragment.implemented(repository));
|
||||
}
|
||||
|
||||
return fragments;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T, ID> EntityInformation<T, ID> getEntityInformation(Class<T> domainClass) {
|
||||
|
||||
VaultPersistentEntity<T> entity = (VaultPersistentEntity<T>) this.operations.getMappingContext()
|
||||
.getPersistentEntity(domainClass);
|
||||
.getRequiredPersistentEntity(domainClass);
|
||||
|
||||
return new MappingVaultEntityInformation<>(entity);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.vault.repository.support;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.history.RevisionMetadata;
|
||||
import org.springframework.vault.support.Versioned;
|
||||
|
||||
/**
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public class VaultRevisionMetadata implements RevisionMetadata<Integer> {
|
||||
|
||||
private final Versioned.Metadata metadata;
|
||||
|
||||
public VaultRevisionMetadata(Versioned<?> versioned) {
|
||||
this(versioned.getRequiredMetadata());
|
||||
}
|
||||
|
||||
public VaultRevisionMetadata(Versioned.Metadata metadata) {
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Integer> getRevisionNumber() {
|
||||
return Optional.of(metadata.getVersion().getVersion());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Instant> getRevisionInstant() {
|
||||
return Optional.of(metadata.getCreatedAt());
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getDelegate() {
|
||||
return (T) metadata;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* Copyright 2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.vault.repository.support;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.history.Revision;
|
||||
import org.springframework.data.history.Revisions;
|
||||
import org.springframework.data.repository.core.EntityInformation;
|
||||
import org.springframework.data.repository.history.RevisionRepository;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.vault.core.VaultKeyValueMetadataOperations;
|
||||
import org.springframework.vault.core.VaultOperations;
|
||||
import org.springframework.vault.core.VaultVersionedKeyValueOperations;
|
||||
import org.springframework.vault.core.util.KeyValueDelegate;
|
||||
import org.springframework.vault.repository.convert.SecretDocument;
|
||||
import org.springframework.vault.repository.convert.VaultConverter;
|
||||
import org.springframework.vault.repository.core.VaultKeyValueTemplate;
|
||||
import org.springframework.vault.support.VaultMetadataResponse;
|
||||
import org.springframework.vault.support.Versioned;
|
||||
|
||||
/**
|
||||
* Vault-based {@link RevisionRepository} providing revision metadata for versioned
|
||||
* secrets.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.4
|
||||
*/
|
||||
public class VaultRevisionRepository<T> implements RevisionRepository<T, String, Integer> {
|
||||
|
||||
private final EntityInformation<T, String> metadata;
|
||||
|
||||
private final String keyspacePath;
|
||||
|
||||
private final VaultVersionedKeyValueOperations operations;
|
||||
|
||||
private final VaultKeyValueMetadataOperations metadataOperations;
|
||||
|
||||
private final VaultConverter converter;
|
||||
|
||||
public VaultRevisionRepository(EntityInformation<T, String> metadata, String keyspace,
|
||||
VaultKeyValueTemplate keyValueTemplate) {
|
||||
|
||||
Assert.notNull(metadata, "EntityInformation must not be null");
|
||||
Assert.notNull(keyValueTemplate, "VaultKeyValueTemplate must not be null");
|
||||
|
||||
this.metadata = metadata;
|
||||
this.converter = keyValueTemplate.getConverter();
|
||||
|
||||
VaultOperations vaultOperations = keyValueTemplate.getVaultOperations();
|
||||
|
||||
KeyValueDelegate delegate = new KeyValueDelegate(vaultOperations);
|
||||
KeyValueDelegate.MountInfo mountInfo = delegate.getMountInfo(keyspace);
|
||||
|
||||
if (!mountInfo.isAvailable()) {
|
||||
throw new IllegalStateException("Mount not available under " + keyspace);
|
||||
}
|
||||
|
||||
if (!delegate.isVersioned(keyspace)) {
|
||||
throw new IllegalStateException("Mount under " + keyspace + " is not versioned");
|
||||
}
|
||||
|
||||
this.keyspacePath = keyspace.substring(mountInfo.getPath().length());
|
||||
this.operations = vaultOperations.opsForVersionedKeyValue(mountInfo.getPath());
|
||||
this.metadataOperations = this.operations.opsForKeyValueMetadata();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Revision<Integer, T>> findLastChangeRevision(String id) {
|
||||
|
||||
Assert.notNull(id, "Identifier must not be null");
|
||||
|
||||
return toRevision(operations.get(getPath(id)), id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Revisions<Integer, T> findRevisions(String id) {
|
||||
|
||||
VaultMetadataResponse metadata = metadataOperations.get(getPath(id));
|
||||
|
||||
if (metadata == null) {
|
||||
return Revisions.none();
|
||||
}
|
||||
|
||||
return Revisions.of(collectRevisions(id, metadata.getVersions()));
|
||||
}
|
||||
|
||||
private List<Revision<Integer, T>> collectRevisions(String id, List<Versioned.Metadata> versions) {
|
||||
|
||||
List<Revision<Integer, T>> revisions = new ArrayList<>();
|
||||
|
||||
for (Versioned.Metadata version : versions) {
|
||||
|
||||
Versioned<Map<String, Object>> versioned = operations.get(getPath(id), version.getVersion());
|
||||
|
||||
if (versioned == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
T entity = versioned.hasData() ? converter.read(this.metadata.getJavaType(), createDocument(id, versioned))
|
||||
: null;
|
||||
revisions.add(Revision.of(new VaultRevisionMetadata(versioned), entity));
|
||||
}
|
||||
return revisions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<Revision<Integer, T>> findRevisions(String id, Pageable pageable) {
|
||||
|
||||
if (pageable.isUnpaged()) {
|
||||
return new PageImpl<>(Collections.emptyList());
|
||||
}
|
||||
|
||||
VaultMetadataResponse metadata = metadataOperations.get(getPath(id));
|
||||
if (metadata == null || pageable.getOffset() > metadata.getVersions().size()) {
|
||||
return Page.empty(pageable);
|
||||
}
|
||||
|
||||
List<Versioned.Metadata> versions = metadata.getVersions();
|
||||
|
||||
int toIndex = Math.min(versions.size(), Math.toIntExact(pageable.getOffset() + pageable.getPageSize()));
|
||||
List<Versioned.Metadata> metadataPage = versions.subList(Math.toIntExact(pageable.getOffset()), toIndex);
|
||||
|
||||
List<Revision<Integer, T>> revisions = collectRevisions(id, metadataPage);
|
||||
|
||||
return new PageImpl<>(revisions, pageable, versions.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Revision<Integer, T>> findRevision(String id, Integer revisionNumber) {
|
||||
|
||||
Assert.notNull(id, "Identifier must not be null");
|
||||
Assert.notNull(revisionNumber, "Revision number must not be null");
|
||||
|
||||
return toRevision(operations.get(getPath(id), Versioned.Version.from(revisionNumber)), id);
|
||||
}
|
||||
|
||||
private Optional<Revision<Integer, T>> toRevision(@Nullable Versioned<Map<String, Object>> versioned, String id) {
|
||||
|
||||
if (versioned == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
T entity = versioned.hasData() ? converter.read(metadata.getJavaType(), createDocument(id, versioned)) : null;
|
||||
return Optional.of(Revision.of(new VaultRevisionMetadata(versioned), entity));
|
||||
}
|
||||
|
||||
private String getPath(String id) {
|
||||
return keyspacePath + "/" + id;
|
||||
}
|
||||
|
||||
private SecretDocument createDocument(String id, Versioned<Map<String, Object>> versioned) {
|
||||
return new SecretDocument(id, versioned.getVersion().getVersion(), versioned.getRequiredData());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -361,8 +361,8 @@ public class VaultCertificateRequest {
|
||||
|
||||
/**
|
||||
* Configure the key format.
|
||||
* @param format the key format to use. Can be {@code pem}, {@code der}, or
|
||||
* {@code pkcs8}
|
||||
* @param privateKeyFormat the key format to use. Can be {@code pem}, {@code der},
|
||||
* or {@code pkcs8}
|
||||
* @return {@code this} {@link VaultCertificateRequestBuilder}.
|
||||
* @since 2.4
|
||||
*/
|
||||
|
||||
@@ -30,7 +30,9 @@ import org.springframework.context.annotation.FilterType;
|
||||
import org.springframework.dao.OptimisticLockingFailureException;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.annotation.Version;
|
||||
import org.springframework.data.history.Revisions;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.data.repository.history.RevisionRepository;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
@@ -105,6 +107,23 @@ class VaultKv2RepositoryIntegrationTests extends IntegrationTestSupport {
|
||||
assertThat(this.versionedRepository.findById("foo-key")).contains(saved);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReportRevisions() {
|
||||
|
||||
VersionedPerson person = new VersionedPerson();
|
||||
person.setId("foo-key");
|
||||
person.setFirstname("bar");
|
||||
|
||||
VersionedPerson saved = this.versionedRepository.save(person);
|
||||
|
||||
saved.setFirstname("baz");
|
||||
this.versionedRepository.save(saved);
|
||||
|
||||
Revisions<Integer, VersionedPerson> revisions = this.versionedRepository.findRevisions(person.getId());
|
||||
|
||||
assertThat(revisions).hasSize(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadAndUpdateVersioned() {
|
||||
|
||||
@@ -228,7 +247,8 @@ class VaultKv2RepositoryIntegrationTests extends IntegrationTestSupport {
|
||||
this.simpleRepository.save(versionedPerson);
|
||||
}
|
||||
|
||||
interface VersionedRepository extends CrudRepository<VersionedPerson, String> {
|
||||
interface VersionedRepository
|
||||
extends CrudRepository<VersionedPerson, String>, RevisionRepository<VersionedPerson, String, Integer> {
|
||||
|
||||
List<VersionedPerson> findByIdStartsWith(String prefix);
|
||||
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
/*
|
||||
* Copyright 2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.vault.repository.support;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.annotation.Version;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.history.Revision;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.vault.VaultException;
|
||||
import org.springframework.vault.core.VaultIntegrationTestConfiguration;
|
||||
import org.springframework.vault.core.VaultSysOperations;
|
||||
import org.springframework.vault.core.VaultTemplate;
|
||||
import org.springframework.vault.repository.configuration.EnableVaultRepositories;
|
||||
import org.springframework.vault.repository.core.MappingVaultEntityInformation;
|
||||
import org.springframework.vault.repository.core.VaultKeyValueTemplate;
|
||||
import org.springframework.vault.repository.mapping.Secret;
|
||||
import org.springframework.vault.repository.mapping.VaultPersistentEntity;
|
||||
import org.springframework.vault.support.VaultMount;
|
||||
import org.springframework.vault.util.IntegrationTestSupport;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration(classes = VaultRevisionRepositoryIntegrationTests.VaultRepositoryTestConfiguration.class)
|
||||
class VaultRevisionRepositoryIntegrationTests extends IntegrationTestSupport {
|
||||
|
||||
@Configuration
|
||||
@EnableVaultRepositories()
|
||||
static class VaultRepositoryTestConfiguration extends VaultIntegrationTestConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@Autowired
|
||||
VaultTemplate vaultTemplate;
|
||||
|
||||
@Autowired
|
||||
VaultKeyValueTemplate keyValueTemplate;
|
||||
|
||||
@BeforeEach
|
||||
void before() {
|
||||
|
||||
VaultSysOperations vaultSysOperations = this.vaultTemplate.opsForSys();
|
||||
|
||||
try {
|
||||
vaultSysOperations.unmount("versioned");
|
||||
}
|
||||
catch (VaultException e) {
|
||||
}
|
||||
|
||||
vaultSysOperations.mount("versioned",
|
||||
VaultMount.builder().type("kv").options(Collections.singletonMap("version", "2")).build());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReportNoRevisions() {
|
||||
|
||||
VaultRevisionRepository<VersionedPerson> repository = getRepository();
|
||||
|
||||
assertThat(repository.findRevision("foo", 1)).isEmpty();
|
||||
assertThat(repository.findRevisions("foo")).isEmpty();
|
||||
assertThat(repository.findRevisions("foo", Pageable.ofSize(2).withPage(1))).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindRevisionMetadata() {
|
||||
|
||||
VaultRevisionRepository<VersionedPerson> repository = getRepository();
|
||||
|
||||
prepareVersions();
|
||||
|
||||
assertThat(repository.findRevision("foo", 1)).get().satisfies(rev -> {
|
||||
assertThat(rev.getEntity().getPassword()).isEqualTo("password-v1");
|
||||
assertThat(rev.getRequiredRevisionInstant()).isNotNull();
|
||||
assertThat(rev.getRequiredRevisionNumber()).isEqualTo(1);
|
||||
});
|
||||
|
||||
assertThat(repository.findRevision("foo", 5)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindLatestRevision() {
|
||||
|
||||
VaultRevisionRepository<VersionedPerson> repository = getRepository();
|
||||
|
||||
prepareVersions();
|
||||
|
||||
assertThat(repository.findLastChangeRevision("foo")).get().satisfies(rev -> {
|
||||
assertThat(rev.getEntity().getPassword()).isEqualTo("password-v4");
|
||||
assertThat(rev.getRequiredRevisionInstant()).isNotNull();
|
||||
assertThat(rev.getRequiredRevisionNumber()).isEqualTo(4);
|
||||
});
|
||||
|
||||
assertThat(repository.findRevision("foo", 5)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindRevisionMetadatas() {
|
||||
|
||||
VaultRevisionRepository<VersionedPerson> repository = getRepository();
|
||||
|
||||
prepareVersions();
|
||||
|
||||
assertThat(repository.findRevisions("foo")).hasSize(4);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindPagedMetadatas() {
|
||||
|
||||
VaultRevisionRepository<VersionedPerson> repository = getRepository();
|
||||
|
||||
prepareVersions();
|
||||
|
||||
Page<Revision<Integer, VersionedPerson>> page1 = repository.findRevisions("foo",
|
||||
Pageable.ofSize(3).withPage(0));
|
||||
|
||||
Page<Revision<Integer, VersionedPerson>> page2 = repository.findRevisions("foo", page1.nextPageable());
|
||||
|
||||
Page<Revision<Integer, VersionedPerson>> page3 = repository.findRevisions("foo", page2.nextPageable());
|
||||
|
||||
assertThat(page1).hasSize(3);
|
||||
assertThat(page1.getTotalElements()).isEqualTo(4);
|
||||
|
||||
assertThat(page2).hasSize(1);
|
||||
assertThat(page2.getTotalElements()).isEqualTo(4);
|
||||
|
||||
assertThat(page3).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindOutOfBoundsPagedMetadatas() {
|
||||
|
||||
VaultRevisionRepository<VersionedPerson> repository = getRepository();
|
||||
|
||||
prepareVersions();
|
||||
|
||||
assertThat(repository.findRevisions("foo", Pageable.ofSize(10).withPage(10))).isEmpty();
|
||||
|
||||
assertThat(repository.findRevisions("foo", Pageable.ofSize(4).withPage(1))).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindDeletedRevisionMetadata() {
|
||||
|
||||
VaultRevisionRepository<VersionedPerson> repository = getRepository();
|
||||
|
||||
VersionedPerson v1 = new VersionedPerson("foo", 0, "password-v1");
|
||||
VersionedPerson v2 = new VersionedPerson("foo", 1, "password-v2");
|
||||
VersionedPerson v3 = new VersionedPerson("foo", 2, "password-v3");
|
||||
VersionedPerson v4 = new VersionedPerson("foo", 3, "password-v4");
|
||||
|
||||
keyValueTemplate.insert(v1);
|
||||
keyValueTemplate.update(v2);
|
||||
keyValueTemplate.update(v3);
|
||||
keyValueTemplate.update(v4);
|
||||
|
||||
keyValueTemplate.delete(v2);
|
||||
|
||||
assertThat(repository.findRevision("foo", 1)).get().satisfies(rev -> {
|
||||
assertThat(rev.getEntity()).isNull();
|
||||
assertThat(rev.getRequiredRevisionInstant()).isNotNull();
|
||||
assertThat(rev.getRequiredRevisionNumber()).isEqualTo(1);
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private VaultRevisionRepository<VersionedPerson> getRepository() {
|
||||
|
||||
VaultPersistentEntity<?> entity = keyValueTemplate.getConverter().getMappingContext()
|
||||
.getRequiredPersistentEntity(VersionedPerson.class);
|
||||
|
||||
return new VaultRevisionRepository<>(new MappingVaultEntityInformation(entity), "versioned/versionedPerson",
|
||||
keyValueTemplate);
|
||||
}
|
||||
|
||||
private void prepareVersions() {
|
||||
|
||||
VersionedPerson v1 = new VersionedPerson("foo", 0, "password-v1");
|
||||
VersionedPerson v2 = new VersionedPerson("foo", 1, "password-v2");
|
||||
VersionedPerson v3 = new VersionedPerson("foo", 2, "password-v3");
|
||||
VersionedPerson v4 = new VersionedPerson("foo", 3, "password-v4");
|
||||
|
||||
keyValueTemplate.insert(v1);
|
||||
keyValueTemplate.update(v2);
|
||||
keyValueTemplate.update(v3);
|
||||
keyValueTemplate.update(v4);
|
||||
}
|
||||
|
||||
@Secret(backend = "versioned")
|
||||
static class VersionedPerson {
|
||||
|
||||
@Id
|
||||
String id;
|
||||
|
||||
@Version
|
||||
int version;
|
||||
|
||||
String password;
|
||||
|
||||
public VersionedPerson() {
|
||||
}
|
||||
|
||||
public VersionedPerson(String id, int version, String password) {
|
||||
this.id = id;
|
||||
this.version = version;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public int getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(int version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -384,3 +384,43 @@ The operation fails with an `OptimisticLockingFailureException` as the version w
|
||||
====
|
||||
|
||||
NOTE: When deleting versioned secrets, delete by Id deletes the most recent secret. Delete by entity deletes the secret at the provided version.
|
||||
|
||||
|
||||
[[vault.repositories.revision-repository]]
|
||||
== Accessing versioned secrets
|
||||
|
||||
Key/Value version 2 secrets engine maintains versions of secrets that can be accessed by implementing https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/repository/history/RevisionRepository.html[`RevisionRepository`] in your Vault repository interface declaration.
|
||||
Revision repositories define lookup methods to obtain revisions for a particular identifier.
|
||||
Identifiers must be `String`.
|
||||
|
||||
|
||||
.Implementing `RevisionRepository`
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
interface RevisionCredentialsRepository extends CrudRepository<Credentials, String>,
|
||||
RevisionRepository<Credentials, String, Integer> <1>
|
||||
{
|
||||
|
||||
}
|
||||
----
|
||||
<1> The first type parameter (`Credentials`) denotes the entity type, the second (`String`) denotes the type of the id property, and the last one (`Integer`) is the type of the revision number. Vault supports only `String` identifiers and `Integer` revision numbers.
|
||||
====
|
||||
|
||||
=== Usage
|
||||
|
||||
You can now use the methods from `RevisionRepository` to query the revisions of the entity, as the following example shows:
|
||||
|
||||
.Using `RevisionRepository`
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
RevisionCredentialsRepository repo = …;
|
||||
|
||||
Revisions<Integer, Credentials> revisions = repo.findRevisions("my-secret-id");
|
||||
|
||||
Page<Revision<Integer, Credentials>> firstPageOfRevisions = repo.findRevisions("my-secret-id", Pageable.ofSize(4));
|
||||
----
|
||||
====
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user