Polishing

Reformat code, convert spaces to tabs, add license and author headers. Reorder methods. Introduce secret path to SecretNotFoundException.

Translate cas failure exception into boolean return state. Create patch body map instead of reusing body response.

See gh-585
Original pull request gh-587
This commit is contained in:
Mark Paluch
2020-10-01 17:26:30 +02:00
parent bc3ff1d47d
commit f46ca3d0f1
7 changed files with 141 additions and 85 deletions

View File

@@ -1,3 +1,18 @@
/*
* Copyright 2020 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.core;
import org.springframework.vault.VaultException;
@@ -6,24 +21,40 @@ import org.springframework.vault.VaultException;
* An exception which is used in case that no secret is found from Vault server.
*
* @author Younghwan Jang
* @author Mark Paluch
* @since 2.3
*/
public class SecretNotFoundException extends VaultException {
/**
* Create a {@code SecretNotFoundException} with the specified detail message.
* @param msg the detail message.
*/
public SecretNotFoundException(String msg) {
super(msg);
}
/**
* Create a {@code SecretNotFoundException} with the specified detail message and nested
* exception.
* @param msg the detail message.
* @param cause the nested exception.
*/
public SecretNotFoundException(String msg, Throwable cause) {
super(msg, cause);
}
private final String path;
/**
* Create a {@code SecretNotFoundException} with the specified detail message.
* @param msg the detail message.
* @param path the canonical data path.
*/
public SecretNotFoundException(String msg, String path) {
super(msg);
this.path = path;
}
/**
* Create a {@code SecretNotFoundException} with the specified detail message and
* nested exception.
* @param msg the detail message.
* @param cause the nested exception.
* @param path the canonical data path.
*/
public SecretNotFoundException(String msg, Throwable cause, String path) {
super(msg, cause);
this.path = path;
}
/**
* @return the path to the requested secret.
*/
public String getPath() {
return this.path;
}
}

View File

@@ -24,13 +24,13 @@ import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.vault.support.VaultResponse;
import org.springframework.vault.support.VaultResponseSupport;
import org.springframework.web.reactive.function.client.WebClientResponseException;
/**
* Default implementation of {@link VaultKeyValueOperations} for the Key/Value backend
* version 1.
*
* @author Mark Paluch
* @author Younghwan Jang
* @since 2.1
* @see KeyValueBackend#KV_1
*/
@@ -99,6 +99,11 @@ class VaultKeyValue1Template extends VaultKeyValueAccessor implements VaultKeyVa
});
}
@Override
public boolean patch(String path, Map<String, ?> patch) {
throw new IllegalStateException("K/V engine mount must be version 2 for patch support");
}
@Override
public void put(String path, Object body) {
@@ -107,11 +112,6 @@ class VaultKeyValue1Template extends VaultKeyValueAccessor implements VaultKeyVa
doWrite(createDataPath(path), body);
}
@Override
public boolean patch(String path, Map<String, ?> kv) {
throw new IllegalStateException("Patch operation is available only in KV secret engine V2");
}
@Override
public KeyValueBackend getApiVersion() {
return KeyValueBackend.KV_1;

View File

@@ -33,7 +33,7 @@ import org.springframework.vault.support.VaultResponseSupport;
*/
abstract class VaultKeyValue2Accessor extends VaultKeyValueAccessor {
private final String path;
final String path;
/**
* Create a new {@link VaultKeyValue2Accessor} given {@link VaultOperations} and the

View File

@@ -17,6 +17,7 @@ package org.springframework.vault.core;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.lang.Nullable;
@@ -30,6 +31,7 @@ import org.springframework.vault.support.VaultResponseSupport;
* version 2.
*
* @author Mark Paluch
* @author Younghwan Jang
* @since 2.1
*/
class VaultKeyValue2Template extends VaultKeyValue2Accessor implements VaultKeyValueOperations {
@@ -69,7 +71,7 @@ class VaultKeyValue2Template extends VaultKeyValue2Accessor implements VaultKeyV
@Nullable
@Override
@SuppressWarnings("unchecked")
@SuppressWarnings({ "unchecked", "rawtypes" })
public <T> VaultResponseSupport<T> get(String path, Class<T> responseType) {
Assert.hasText(path, "Path must not be empty");
@@ -83,6 +85,47 @@ class VaultKeyValue2Template extends VaultKeyValue2Accessor implements VaultKeyV
});
}
@Override
public boolean patch(String path, Map<String, ?> patch) {
Assert.hasText(path, "Path must not be empty");
Assert.notNull(patch, "Patch body must not be null");
// To do patch operation, we need to do a read operation first
VaultResponse readResponse = get(path);
if (readResponse == null || readResponse.getData() == null) {
throw new SecretNotFoundException(
String.format("No data found at %s; patch only works on existing data", createDataPath(path)),
String.format("%s/%s", this.path, path));
}
if (readResponse.getMetadata() == null) {
throw new VaultException("Metadata must not be null");
}
Map<String, Object> metadata = readResponse.getMetadata();
Map<String, Object> data = new LinkedHashMap<>(readResponse.getRequiredData());
data.putAll(patch);
Map<String, Object> body = new HashMap<>();
body.put("data", data);
body.put("options", Collections.singletonMap("cas", metadata.get("version")));
try {
doWrite(createDataPath(path), body);
return true;
}
catch (VaultException e) {
if (e.getMessage() != null && (e.getMessage().contains("check-and-set")
|| e.getMessage().contains("did not match the current version"))) {
return false;
}
throw e;
}
}
@Override
public void put(String path, Object body) {
@@ -91,38 +134,4 @@ class VaultKeyValue2Template extends VaultKeyValue2Accessor implements VaultKeyV
doWrite(createDataPath(path), Collections.singletonMap("data", body));
}
/**
* Performs a KV Patch operation.
* @param path must not be {@literal null} or empty.
* @param kv New key value map to be updated
* @since 2.3
*/
@Override
public boolean patch(String path, Map<String, ?> kv) {
Assert.hasText(path, "Path must not be empty");
// To do patch operation, we need to do a read operation first
VaultResponse readResponse = get(path);
if (null == readResponse) {
throw new VaultException("VaultResponse must not be null");
} else if (null == readResponse.getData()) {
throw new SecretNotFoundException("No data found at %s; patch only works on existing data");
} else if (null == readResponse.getMetadata()) {
throw new VaultException("Metadata must not be null");
}
Map<String, Object> data = readResponse.getData();
Map<String, Object> metadata = readResponse.getMetadata();
kv.forEach(data::put);
Map<String, Object> body = new HashMap<>();
body.put("data", data);
body.put("options", Collections.singletonMap("cas", metadata.get("version")));
VaultResponse writeResponse = doWrite(createDataPath(path), body);
if (null == writeResponse) {
return false;
}
Map<String, Object> writeResponseData = writeResponse.getData();
return null != writeResponseData;
}
}

View File

@@ -110,7 +110,8 @@ abstract class VaultKeyValueAccessor implements VaultKeyValueOperationsSupport {
JsonNode jsonNode = getJsonNode(response);
JsonNode jsonMeta = response.getRequiredData().at("/metadata");
response.setMetadata(mapper.convertValue(jsonMeta, new TypeReference<Map<String, Object>>() {}));
response.setMetadata(this.mapper.convertValue(jsonMeta, new TypeReference<Map<String, Object>>() {
}));
return mappingFunction.apply(response, deserialize(jsonNode, deserializeAs));
}

View File

@@ -15,12 +15,12 @@
*/
package org.springframework.vault.core;
import java.util.Map;
import org.springframework.lang.Nullable;
import org.springframework.vault.support.VaultResponse;
import org.springframework.vault.support.VaultResponseSupport;
import java.util.Map;
/**
* Interface that specifies a basic set of Vault operations using Vault's Key/Value secret
* backend. Paths used in this operations interface are relative and outgoing requests
@@ -31,6 +31,7 @@ import java.util.Map;
* {@link VaultVersionedKeyValueOperations} in such cases instead.
*
* @author Mark Paluch
* @author Younghwan Jang
* @since 2.1
* @see VaultVersionedKeyValueOperations
* @see VaultKeyValueOperationsSupport.KeyValueBackend
@@ -54,6 +55,16 @@ public interface VaultKeyValueOperations extends VaultKeyValueOperationsSupport
@Nullable
<T> VaultResponseSupport<T> get(String path, Class<T> responseType);
/**
* Update the secret at {@code path} without removing the existing secrets. Requires a
* Key-Value version 2 mount to ensure an atomic update.
* @param path must not be {@literal null}.
* @param patch must not be {@literal null}.
* @return {@code true} if the patch operation is successful, {@code false} otherwise.
* @since 2.3
*/
boolean patch(String path, Map<String, ?> patch);
/**
* Write the secret at {@code path}.
* @param path must not be {@literal null}.
@@ -61,12 +72,4 @@ public interface VaultKeyValueOperations extends VaultKeyValueOperationsSupport
*/
void put(String path, Object body);
/**
* Updates the secret at {@code path} without removing the existing secrets.
* @param path must not be {@literal null}.
* @param kv must not be {@literal null}.
* @return true if the patch operation is successful, false otherwise.
*/
boolean patch(String path, Map<String, ?> kv);
}

View File

@@ -15,7 +15,10 @@
*/
package org.springframework.vault.core;
import org.junit.jupiter.api.Assertions;
import java.util.Collections;
import java.util.Map;
import java.util.UUID;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -24,19 +27,15 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.vault.core.VaultKeyValueOperationsSupport.KeyValueBackend;
import org.springframework.vault.support.VaultResponse;
import java.util.Collections;
import java.util.Map;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.assertj.core.api.Fail.fail;
/**
* Integration tests for {@link VaultKeyValue2Template} using the versioned Key/Value (k/v
* version 2) backend.
*
* @author Mark Paluch
* @author Younghwan Jang
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = VaultIntegrationTestConfiguration.class)
@@ -48,8 +47,10 @@ class VaultKeyValueTemplateVersionedIntegrationTests extends AbstractVaultKeyVal
@Test
void shouldPatchSecret() {
final String oldKey = "key";
final String newKey = "newKey";
String oldKey = "key";
String newKey = "newKey";
Map<String, String> secret = Collections.singletonMap(oldKey, "value");
String key = UUID.randomUUID().toString();
@@ -58,15 +59,26 @@ class VaultKeyValueTemplateVersionedIntegrationTests extends AbstractVaultKeyVal
Map<String, String> newSecret = Collections.singletonMap(newKey, "newValue");
assertTrue(this.kvOperations.patch(key, newSecret));
assertThat(this.kvOperations.patch(key, newSecret)).isTrue();
assertThat(this.kvOperations.list("/")).contains(key);
VaultResponse vaultResponse = this.kvOperations.get(key);
assertNotNull(vaultResponse);
Map<String, Object> data = vaultResponse.getData();
assertNotNull(data);
assertThat(data).containsKey(oldKey);
assertThat(data).containsKey(newKey);
Map<String, Object> data = vaultResponse.getRequiredData();
assertThat(data).containsKey(oldKey).containsKey(newKey);
}
@Test
void patchShouldFailWithSecretNotFoundException() {
try {
this.kvOperations.patch("unknown", Collections.singletonMap("foo", "newValue"));
fail("missing SecretNotFoundException");
}
catch (SecretNotFoundException e) {
assertThat(e).hasMessageContaining("versioned/data/unknown");
assertThat(e.getPath()).isEqualTo("versioned/unknown");
}
}
}