Adding support of KV Patch operation
Closes gh-585 Original pull request gh-587
This commit is contained in:
committed by
Mark Paluch
parent
7274675483
commit
bc3ff1d47d
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user