Skip to content

Commit 7d4b718

Browse files
committed
Add support for RevisionRepository.
Vault repositories can now implement RevisionRepository to access older secret revisions. See gh-593
1 parent f4fd3ee commit 7d4b718

File tree

10 files changed

+613
-22
lines changed

10 files changed

+613
-22
lines changed

spring-vault-core/src/main/java/org/springframework/vault/repository/convert/SecretDocument.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.springframework.util.Assert;
2323
import org.springframework.util.ObjectUtils;
2424
import org.springframework.vault.support.VaultResponse;
25+
import org.springframework.vault.support.Versioned;
2526

2627
/**
2728
* Vault database exchange object containing data before/after it's exchanged with Vault.

spring-vault-core/src/main/java/org/springframework/vault/repository/core/VaultKeyValueAdapter.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,14 @@ private VaultKeyValueKeyspaceAccessor getAccessor(String keyspace) {
268268

269269
}
270270

271+
public VaultConverter getConverter() {
272+
return this.vaultConverter;
273+
}
274+
275+
public VaultOperations getVaultOperations() {
276+
return this.vaultOperations;
277+
}
278+
271279
static abstract class VaultKeyValueKeyspaceAccessor {
272280

273281
private final KeyValueDelegate.MountInfo mountInfo;
@@ -384,8 +392,7 @@ SecretDocument get(String id) {
384392
return null;
385393
}
386394

387-
return new SecretDocument(id, versioned.getVersion()
388-
.getVersion(), versioned.getRequiredData());
395+
return new SecretDocument(id, versioned.getVersion().getVersion(), versioned.getRequiredData());
389396
}
390397

391398
@Override
@@ -401,14 +408,12 @@ SecretDocument put(SecretDocument secretDocument) {
401408
metadata = operations.put(createPath(secretDocument.getRequiredId()), secretDocument.getBody());
402409
}
403410

404-
return new SecretDocument(secretDocument.getRequiredId(), metadata.getVersion()
405-
.getVersion(),
411+
return new SecretDocument(secretDocument.getRequiredId(), metadata.getVersion().getVersion(),
406412
secretDocument.getBody());
407413
}
408414
catch (VaultException e) {
409415
if (e.getMessage() != null
410-
&& e.getMessage()
411-
.contains("check-and-set parameter did not match the current version")) {
416+
&& e.getMessage().contains("check-and-set parameter did not match the current version")) {
412417
throw new OptimisticLockingFailureException(e.getMessage(), e);
413418
}
414419

spring-vault-core/src/main/java/org/springframework/vault/repository/core/VaultKeyValueTemplate.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import org.springframework.util.Assert;
2929
import org.springframework.util.ClassUtils;
3030
import org.springframework.util.CollectionUtils;
31+
import org.springframework.vault.core.VaultOperations;
32+
import org.springframework.vault.repository.convert.VaultConverter;
3133
import org.springframework.vault.repository.mapping.VaultMappingContext;
3234

3335
/**
@@ -44,8 +46,7 @@ public class VaultKeyValueTemplate extends KeyValueTemplate {
4446
private boolean publishEvents = true;
4547

4648
@SuppressWarnings("rawtypes")
47-
private Set<Class<? extends KeyValueEvent>> eventTypesToPublish = Collections
48-
.emptySet();
49+
private Set<Class<? extends KeyValueEvent>> eventTypesToPublish = Collections.emptySet();
4950

5051
/**
5152
* Create a new {@link VaultKeyValueTemplate} given {@link KeyValueAdapter} and
@@ -163,8 +164,7 @@ private String resolveKeySpace(Class<?> type) {
163164

164165
@SuppressWarnings("rawtypes")
165166
private KeyValuePersistentEntity<?, ?> getEntity(Class<?> type) {
166-
return (KeyValuePersistentEntity) getMappingContext()
167-
.getRequiredPersistentEntity(type);
167+
return (KeyValuePersistentEntity) getMappingContext().getRequiredPersistentEntity(type);
168168
}
169169

170170
@SuppressWarnings("rawtypes")
@@ -179,4 +179,12 @@ private void potentiallyPublishEvent(KeyValueEvent event) {
179179
}
180180
}
181181

182+
public VaultConverter getConverter() {
183+
return execute(adapter -> ((VaultKeyValueAdapter) adapter).getConverter());
184+
}
185+
186+
public VaultOperations getVaultOperations() {
187+
return execute(adapter -> ((VaultKeyValueAdapter) adapter).getVaultOperations());
188+
}
189+
182190
}

spring-vault-core/src/main/java/org/springframework/vault/repository/support/VaultRepositoryFactory.java

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,15 @@
1919
import org.springframework.data.keyvalue.repository.query.KeyValuePartTreeQuery;
2020
import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactory;
2121
import org.springframework.data.repository.core.EntityInformation;
22+
import org.springframework.data.repository.core.RepositoryMetadata;
23+
import org.springframework.data.repository.core.support.RepositoryComposition;
2224
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
25+
import org.springframework.data.repository.core.support.RepositoryFragment;
26+
import org.springframework.data.repository.history.RevisionRepository;
2327
import org.springframework.data.repository.query.RepositoryQuery;
2428
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
2529
import org.springframework.vault.repository.core.MappingVaultEntityInformation;
30+
import org.springframework.vault.repository.core.VaultKeyValueTemplate;
2631
import org.springframework.vault.repository.mapping.VaultPersistentEntity;
2732
import org.springframework.vault.repository.query.VaultQueryCreator;
2833

@@ -54,12 +59,35 @@ public VaultRepositoryFactory(KeyValueOperations keyValueOperations,
5459
this.operations = keyValueOperations;
5560
}
5661

62+
@Override
63+
protected RepositoryComposition.RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata,
64+
KeyValueOperations operations) {
65+
66+
RepositoryComposition.RepositoryFragments fragments = super.getRepositoryFragments(metadata, operations);
67+
68+
if (RevisionRepository.class.isAssignableFrom(metadata.getRepositoryInterface())
69+
&& operations instanceof VaultKeyValueTemplate) {
70+
71+
VaultKeyValueTemplate template = (VaultKeyValueTemplate) operations;
72+
73+
VaultPersistentEntity<?> entity = (VaultPersistentEntity<?>) this.operations.getMappingContext()
74+
.getRequiredPersistentEntity(metadata.getDomainType());
75+
EntityInformation<?, String> entityInformation = getEntityInformation(metadata.getDomainType());
76+
VaultRevisionRepository<?> repository = new VaultRevisionRepository<>(entityInformation,
77+
entity.getKeySpace(), template);
78+
79+
return fragments.append(RepositoryFragment.implemented(repository));
80+
}
81+
82+
return fragments;
83+
}
84+
5785
@Override
5886
@SuppressWarnings("unchecked")
5987
public <T, ID> EntityInformation<T, ID> getEntityInformation(Class<T> domainClass) {
6088

6189
VaultPersistentEntity<T> entity = (VaultPersistentEntity<T>) this.operations.getMappingContext()
62-
.getPersistentEntity(domainClass);
90+
.getRequiredPersistentEntity(domainClass);
6391

6492
return new MappingVaultEntityInformation<>(entity);
6593
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.vault.repository.support;
17+
18+
import java.time.Instant;
19+
import java.util.Optional;
20+
21+
import org.springframework.data.history.RevisionMetadata;
22+
import org.springframework.vault.support.Versioned;
23+
24+
/**
25+
* @author Mark Paluch
26+
*/
27+
public class VaultRevisionMetadata implements RevisionMetadata<Integer> {
28+
29+
private final Versioned.Metadata metadata;
30+
31+
public VaultRevisionMetadata(Versioned<?> versioned) {
32+
this(versioned.getRequiredMetadata());
33+
}
34+
35+
public VaultRevisionMetadata(Versioned.Metadata metadata) {
36+
this.metadata = metadata;
37+
}
38+
39+
@Override
40+
public Optional<Integer> getRevisionNumber() {
41+
return Optional.of(metadata.getVersion().getVersion());
42+
}
43+
44+
@Override
45+
public Optional<Instant> getRevisionInstant() {
46+
return Optional.of(metadata.getCreatedAt());
47+
}
48+
49+
@Override
50+
public <T> T getDelegate() {
51+
return (T) metadata;
52+
}
53+
54+
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.vault.repository.support;
17+
18+
import java.util.ArrayList;
19+
import java.util.Collections;
20+
import java.util.List;
21+
import java.util.Map;
22+
import java.util.Optional;
23+
24+
import org.springframework.data.domain.Page;
25+
import org.springframework.data.domain.PageImpl;
26+
import org.springframework.data.domain.Pageable;
27+
import org.springframework.data.history.Revision;
28+
import org.springframework.data.history.Revisions;
29+
import org.springframework.data.repository.core.EntityInformation;
30+
import org.springframework.data.repository.history.RevisionRepository;
31+
import org.springframework.lang.Nullable;
32+
import org.springframework.util.Assert;
33+
import org.springframework.vault.core.VaultKeyValueMetadataOperations;
34+
import org.springframework.vault.core.VaultOperations;
35+
import org.springframework.vault.core.VaultVersionedKeyValueOperations;
36+
import org.springframework.vault.core.util.KeyValueDelegate;
37+
import org.springframework.vault.repository.convert.SecretDocument;
38+
import org.springframework.vault.repository.convert.VaultConverter;
39+
import org.springframework.vault.repository.core.VaultKeyValueTemplate;
40+
import org.springframework.vault.support.VaultMetadataResponse;
41+
import org.springframework.vault.support.Versioned;
42+
43+
/**
44+
* Vault-based {@link RevisionRepository} providing revision metadata for versioned
45+
* secrets.
46+
*
47+
* @author Mark Paluch
48+
* @since 2.4
49+
*/
50+
public class VaultRevisionRepository<T> implements RevisionRepository<T, String, Integer> {
51+
52+
private final EntityInformation<T, String> metadata;
53+
54+
private final String keyspacePath;
55+
56+
private final VaultVersionedKeyValueOperations operations;
57+
58+
private final VaultKeyValueMetadataOperations metadataOperations;
59+
60+
private final VaultConverter converter;
61+
62+
public VaultRevisionRepository(EntityInformation<T, String> metadata, String keyspace,
63+
VaultKeyValueTemplate keyValueTemplate) {
64+
65+
Assert.notNull(metadata, "EntityInformation must not be null");
66+
Assert.notNull(keyValueTemplate, "VaultKeyValueTemplate must not be null");
67+
68+
this.metadata = metadata;
69+
this.converter = keyValueTemplate.getConverter();
70+
71+
VaultOperations vaultOperations = keyValueTemplate.getVaultOperations();
72+
73+
KeyValueDelegate delegate = new KeyValueDelegate(vaultOperations);
74+
KeyValueDelegate.MountInfo mountInfo = delegate.getMountInfo(keyspace);
75+
76+
if (!mountInfo.isAvailable()) {
77+
throw new IllegalStateException("Mount not available under " + keyspace);
78+
}
79+
80+
if (!delegate.isVersioned(keyspace)) {
81+
throw new IllegalStateException("Mount under " + keyspace + " is not versioned");
82+
}
83+
84+
this.keyspacePath = keyspace.substring(mountInfo.getPath().length());
85+
this.operations = vaultOperations.opsForVersionedKeyValue(mountInfo.getPath());
86+
this.metadataOperations = this.operations.opsForKeyValueMetadata();
87+
}
88+
89+
@Override
90+
public Optional<Revision<Integer, T>> findLastChangeRevision(String id) {
91+
92+
Assert.notNull(id, "Identifier must not be null");
93+
94+
return toRevision(operations.get(getPath(id)), id);
95+
}
96+
97+
@Override
98+
public Revisions<Integer, T> findRevisions(String id) {
99+
100+
VaultMetadataResponse metadata = metadataOperations.get(getPath(id));
101+
102+
if (metadata == null) {
103+
return Revisions.none();
104+
}
105+
106+
return Revisions.of(collectRevisions(id, metadata.getVersions()));
107+
}
108+
109+
private List<Revision<Integer, T>> collectRevisions(String id, List<Versioned.Metadata> versions) {
110+
111+
List<Revision<Integer, T>> revisions = new ArrayList<>();
112+
113+
for (Versioned.Metadata version : versions) {
114+
115+
Versioned<Map<String, Object>> versioned = operations.get(getPath(id), version.getVersion());
116+
117+
if (versioned == null) {
118+
continue;
119+
}
120+
121+
T entity = versioned.hasData() ? converter.read(this.metadata.getJavaType(), createDocument(id, versioned))
122+
: null;
123+
revisions.add(Revision.of(new VaultRevisionMetadata(versioned), entity));
124+
}
125+
return revisions;
126+
}
127+
128+
@Override
129+
public Page<Revision<Integer, T>> findRevisions(String id, Pageable pageable) {
130+
131+
if (pageable.isUnpaged()) {
132+
return new PageImpl<>(Collections.emptyList());
133+
}
134+
135+
VaultMetadataResponse metadata = metadataOperations.get(getPath(id));
136+
if (metadata == null || pageable.getOffset() > metadata.getVersions().size()) {
137+
return Page.empty(pageable);
138+
}
139+
140+
List<Versioned.Metadata> versions = metadata.getVersions();
141+
142+
int toIndex = Math.min(versions.size(), Math.toIntExact(pageable.getOffset() + pageable.getPageSize()));
143+
List<Versioned.Metadata> metadataPage = versions.subList(Math.toIntExact(pageable.getOffset()), toIndex);
144+
145+
List<Revision<Integer, T>> revisions = collectRevisions(id, metadataPage);
146+
147+
return new PageImpl<>(revisions, pageable, versions.size());
148+
}
149+
150+
@Override
151+
public Optional<Revision<Integer, T>> findRevision(String id, Integer revisionNumber) {
152+
153+
Assert.notNull(id, "Identifier must not be null");
154+
Assert.notNull(revisionNumber, "Revision number must not be null");
155+
156+
return toRevision(operations.get(getPath(id), Versioned.Version.from(revisionNumber)), id);
157+
}
158+
159+
private Optional<Revision<Integer, T>> toRevision(@Nullable Versioned<Map<String, Object>> versioned, String id) {
160+
161+
if (versioned == null) {
162+
return Optional.empty();
163+
}
164+
165+
T entity = versioned.hasData() ? converter.read(metadata.getJavaType(), createDocument(id, versioned)) : null;
166+
return Optional.of(Revision.of(new VaultRevisionMetadata(versioned), entity));
167+
}
168+
169+
private String getPath(String id) {
170+
return keyspacePath + "/" + id;
171+
}
172+
173+
private SecretDocument createDocument(String id, Versioned<Map<String, Object>> versioned) {
174+
return new SecretDocument(id, versioned.getVersion().getVersion(), versioned.getRequiredData());
175+
}
176+
177+
}

spring-vault-core/src/main/java/org/springframework/vault/support/VaultCertificateRequest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -376,8 +376,8 @@ public VaultCertificateRequestBuilder format(String format) {
376376

377377
/**
378378
* Configure the key format.
379-
* @param format the key format to use. Can be {@code pem}, {@code der}, or
380-
* {@code pkcs8}
379+
* @param privateKeyFormat the key format to use. Can be {@code pem}, {@code der},
380+
* or {@code pkcs8}
381381
* @return {@code this} {@link VaultCertificateRequestBuilder}.
382382
* @since 2.4
383383
*/

0 commit comments

Comments
 (0)