diff --git a/spring-vault-core/src/main/java/org/springframework/vault/repository/convert/SecretDocument.java b/spring-vault-core/src/main/java/org/springframework/vault/repository/convert/SecretDocument.java index f689a532..ad3c5d1c 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/repository/convert/SecretDocument.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/repository/convert/SecretDocument.java @@ -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. diff --git a/spring-vault-core/src/main/java/org/springframework/vault/repository/core/VaultKeyValueAdapter.java b/spring-vault-core/src/main/java/org/springframework/vault/repository/core/VaultKeyValueAdapter.java index 104eee1f..507e59ec 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/repository/core/VaultKeyValueAdapter.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/repository/core/VaultKeyValueAdapter.java @@ -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; diff --git a/spring-vault-core/src/main/java/org/springframework/vault/repository/core/VaultKeyValueTemplate.java b/spring-vault-core/src/main/java/org/springframework/vault/repository/core/VaultKeyValueTemplate.java index 4cc6c8ba..6808c15d 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/repository/core/VaultKeyValueTemplate.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/repository/core/VaultKeyValueTemplate.java @@ -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()); + } + } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/repository/support/VaultRepositoryFactory.java b/spring-vault-core/src/main/java/org/springframework/vault/repository/support/VaultRepositoryFactory.java index 135115a3..01bc4a4f 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/repository/support/VaultRepositoryFactory.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/repository/support/VaultRepositoryFactory.java @@ -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 entityInformation = getEntityInformation(metadata.getDomainType()); + VaultRevisionRepository repository = new VaultRevisionRepository<>(entityInformation, + entity.getKeySpace(), template); + + return fragments.append(RepositoryFragment.implemented(repository)); + } + + return fragments; + } + @Override @SuppressWarnings("unchecked") public EntityInformation getEntityInformation(Class domainClass) { VaultPersistentEntity entity = (VaultPersistentEntity) this.operations.getMappingContext() - .getPersistentEntity(domainClass); + .getRequiredPersistentEntity(domainClass); return new MappingVaultEntityInformation<>(entity); } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/repository/support/VaultRevisionMetadata.java b/spring-vault-core/src/main/java/org/springframework/vault/repository/support/VaultRevisionMetadata.java new file mode 100644 index 00000000..51f729b0 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/repository/support/VaultRevisionMetadata.java @@ -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 { + + private final Versioned.Metadata metadata; + + public VaultRevisionMetadata(Versioned versioned) { + this(versioned.getRequiredMetadata()); + } + + public VaultRevisionMetadata(Versioned.Metadata metadata) { + this.metadata = metadata; + } + + @Override + public Optional getRevisionNumber() { + return Optional.of(metadata.getVersion().getVersion()); + } + + @Override + public Optional getRevisionInstant() { + return Optional.of(metadata.getCreatedAt()); + } + + @Override + public T getDelegate() { + return (T) metadata; + } + +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/repository/support/VaultRevisionRepository.java b/spring-vault-core/src/main/java/org/springframework/vault/repository/support/VaultRevisionRepository.java new file mode 100644 index 00000000..4911aa0e --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/repository/support/VaultRevisionRepository.java @@ -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 implements RevisionRepository { + + private final EntityInformation metadata; + + private final String keyspacePath; + + private final VaultVersionedKeyValueOperations operations; + + private final VaultKeyValueMetadataOperations metadataOperations; + + private final VaultConverter converter; + + public VaultRevisionRepository(EntityInformation 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> findLastChangeRevision(String id) { + + Assert.notNull(id, "Identifier must not be null"); + + return toRevision(operations.get(getPath(id)), id); + } + + @Override + public Revisions findRevisions(String id) { + + VaultMetadataResponse metadata = metadataOperations.get(getPath(id)); + + if (metadata == null) { + return Revisions.none(); + } + + return Revisions.of(collectRevisions(id, metadata.getVersions())); + } + + private List> collectRevisions(String id, List versions) { + + List> revisions = new ArrayList<>(); + + for (Versioned.Metadata version : versions) { + + Versioned> 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> 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 versions = metadata.getVersions(); + + int toIndex = Math.min(versions.size(), Math.toIntExact(pageable.getOffset() + pageable.getPageSize())); + List metadataPage = versions.subList(Math.toIntExact(pageable.getOffset()), toIndex); + + List> revisions = collectRevisions(id, metadataPage); + + return new PageImpl<>(revisions, pageable, versions.size()); + } + + @Override + public Optional> 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> toRevision(@Nullable Versioned> 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> versioned) { + return new SecretDocument(id, versioned.getVersion().getVersion(), versioned.getRequiredData()); + } + +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultCertificateRequest.java b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultCertificateRequest.java index c6e9f9e3..62f38104 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultCertificateRequest.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultCertificateRequest.java @@ -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 */ diff --git a/spring-vault-core/src/test/java/org/springframework/vault/repository/VaultKv2RepositoryIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/repository/VaultKv2RepositoryIntegrationTests.java index 0cc14662..ad2f3413 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/repository/VaultKv2RepositoryIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/repository/VaultKv2RepositoryIntegrationTests.java @@ -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 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 { + interface VersionedRepository + extends CrudRepository, RevisionRepository { List findByIdStartsWith(String prefix); diff --git a/spring-vault-core/src/test/java/org/springframework/vault/repository/support/VaultRevisionRepositoryIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/repository/support/VaultRevisionRepositoryIntegrationTests.java new file mode 100644 index 00000000..1a45ca1c --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/repository/support/VaultRevisionRepositoryIntegrationTests.java @@ -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 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 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 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 repository = getRepository(); + + prepareVersions(); + + assertThat(repository.findRevisions("foo")).hasSize(4); + } + + @Test + void shouldFindPagedMetadatas() { + + VaultRevisionRepository repository = getRepository(); + + prepareVersions(); + + Page> page1 = repository.findRevisions("foo", + Pageable.ofSize(3).withPage(0)); + + Page> page2 = repository.findRevisions("foo", page1.nextPageable()); + + Page> 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 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 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 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; + } + + } + +} diff --git a/src/main/asciidoc/reference/vault-repositories.adoc b/src/main/asciidoc/reference/vault-repositories.adoc index e20ac5fc..43242a84 100644 --- a/src/main/asciidoc/reference/vault-repositories.adoc +++ b/src/main/asciidoc/reference/vault-repositories.adoc @@ -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, + RevisionRepository <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 revisions = repo.findRevisions("my-secret-id"); + +Page> firstPageOfRevisions = repo.findRevisions("my-secret-id", Pageable.ofSize(4)); +---- +==== + +