Adding support of KV Patch operation

Closes gh-585
Original pull request gh-587
This commit is contained in:
Younghwan Jang
2020-09-23 23:20:58 -07:00
committed by Mark Paluch
parent 7274675483
commit bc3ff1d47d
6 changed files with 119 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
package org.springframework.vault.core;
import org.springframework.vault.VaultException;
/**
* An exception which is used in case that no secret is found from Vault server.
*
* @author Younghwan Jang
* @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);
}
}

View File

@@ -24,6 +24,7 @@ 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
@@ -106,6 +107,11 @@ 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

@@ -16,10 +16,12 @@
package org.springframework.vault.core;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.vault.VaultException;
import org.springframework.vault.support.VaultResponse;
import org.springframework.vault.support.VaultResponseSupport;
@@ -89,4 +91,38 @@ 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

@@ -16,10 +16,12 @@
package org.springframework.vault.core;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Function;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -107,6 +109,8 @@ abstract class VaultKeyValueAccessor implements VaultKeyValueOperationsSupport {
if (response != null) {
JsonNode jsonNode = getJsonNode(response);
JsonNode jsonMeta = response.getRequiredData().at("/metadata");
response.setMetadata(mapper.convertValue(jsonMeta, new TypeReference<Map<String, Object>>() {}));
return mappingFunction.apply(response, deserialize(jsonNode, deserializeAs));
}

View File

@@ -19,6 +19,8 @@ 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
@@ -59,4 +61,12 @@ 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,11 +15,22 @@
*/
package org.springframework.vault.core;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.test.context.ContextConfiguration;
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;
/**
* Integration tests for {@link VaultKeyValue2Template} using the versioned Key/Value (k/v
@@ -35,4 +46,27 @@ class VaultKeyValueTemplateVersionedIntegrationTests extends AbstractVaultKeyVal
super("versioned", KeyValueBackend.versioned());
}
@Test
void shouldPatchSecret() {
final String oldKey = "key";
final String newKey = "newKey";
Map<String, String> secret = Collections.singletonMap(oldKey, "value");
String key = UUID.randomUUID().toString();
this.kvOperations.put(key, secret);
Map<String, String> newSecret = Collections.singletonMap(newKey, "newValue");
assertTrue(this.kvOperations.patch(key, newSecret));
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);
}
}