Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

embedded vault: add option to enable Compare and Set operations #1051

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions embedded-vault/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
* `embedded.vault.token` `(default is '00000000-0000-0000-0000-000000000000')`
* `embedded.vault.path` `(default is 'secret/application')`
* `embedded.vault.secrets` `(Map, default is empty)`
* `embedded.vault.cas-enabled` `(true|false, default is false, enables Check and Set operations for mount)`
* `embedded.vault.cas-enabled-for-sub-paths` `(enables cas for specified, example: sub-paths sub-path1, sub-path2)`

==== Produces

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;

import static com.playtika.test.common.utils.ContainerUtils.configureCommonsAndStart;
import static com.playtika.test.vault.VaultProperties.BEAN_NAME_EMBEDDED_VAULT;
Expand Down Expand Up @@ -45,6 +46,15 @@ public VaultContainer vault(ConfigurableEnvironment environment, VaultProperties
vault.withSecretInVault(properties.getPath(), secrets[0], Arrays.copyOfRange(secrets, 1, secrets.length));
}

if (properties.isCasEnabled()) {
log.info("Enabling cas for mount secret");
vault.withInitCommand("write secret/config cas_required=true");
}

if (!properties.getCasEnabledForSubPaths().isEmpty()) {
enableCasForSubPaths(properties.getCasEnabledForSubPaths(), vault);
}

vault = (VaultContainer) configureCommonsAndStart(vault, properties, log);
registerVaultEnvironment(vault, environment, properties);
return vault;
Expand All @@ -64,4 +74,14 @@ private void registerVaultEnvironment(VaultContainer vault, ConfigurableEnvironm
MapPropertySource propertySource = new MapPropertySource("embeddedVaultInfo", map);
environment.getPropertySources().addFirst(propertySource);
}


private void enableCasForSubPaths(List<String> subPaths, VaultContainer vault) {
for (String subPath : subPaths) {
if (!subPath.isEmpty()) {
log.info("Vault: Enabling cas for sub path {}", subPath);
vault.withInitCommand("kv metadata put -cas-required=true secret/" + subPath);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
import lombok.EqualsAndHashCode;
import org.springframework.boot.context.properties.ConfigurationProperties;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Data
Expand All @@ -23,6 +25,8 @@ public class VaultProperties extends CommonContainerProperties {
private String token = "00000000-0000-0000-0000-000000000000";
private String path = "secret/application";
private final Map<String, String> secrets = new HashMap<>();
private boolean casEnabled = false;
private List<String> casEnabledForSubPaths = new ArrayList<>();

// https://hub.docker.com/_/vault
@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package com.playtika.test.vault;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.vault.VaultException;
import org.springframework.vault.core.VaultOperations;
import org.springframework.vault.core.VaultTemplate;
import org.springframework.vault.support.Versioned;

import java.util.HashMap;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.entry;

@SpringBootTest(properties = {
"embedded.vault.secrets.secret_one=password1",
"embedded.vault.cas-enabled-for-sub-paths=sub-path1, sub-path2"
}, classes = EmbeddedVaultWithCasEnabledForSubPathsTest.TestConfiguration.class)
public class EmbeddedVaultWithCasEnabledForSubPathsTest {
private static final String SECRET_ENTRY_KEY = "cas_key";
private static final String SECRET_ENTRY_VALUE = "cas_val";

private static final String CHECK_AND_SET_REQUIRED_ERROR = "check-and-set parameter required for this call";
private static final String SECRET_MOUNT_NAME = "secret";
private static final String SECRET_MOUNT_PATH = SECRET_MOUNT_NAME + "/data/";
private static final String SUB_PATH1 = "sub-path1";
private static final String SUB_PATH2 = "sub-path2";

@Autowired
private ConfigurableEnvironment environment;

@Autowired
private VaultOperations vaultOperations;

@Autowired
private VaultTemplate vaultTemplate;

@Test
public void propertiesAreAvailable() {
assertThat(environment.getProperty("embedded.vault.host")).isNotEmpty();
assertThat(environment.getProperty("embedded.vault.port")).isNotEmpty();
assertThat(environment.getProperty("embedded.vault.token")).isNotEmpty();
assertThat(environment.getProperty("secret_one")).isNotEmpty();
assertThat(environment.getProperty("embedded.vault.cas-enabled-for-sub-paths")).isNotEmpty();
}

@Test
public void shouldFailOnWriteWhenCasEnabledButNotPassed() {
Map<String, Object> body = new HashMap<>();
body.put("data", buildCredentialsEntity());

assertThatThrownBy(() -> vaultTemplate.write(SECRET_MOUNT_PATH + SUB_PATH1, body))
.isExactlyInstanceOf(VaultException.class)
.hasMessageContaining(CHECK_AND_SET_REQUIRED_ERROR);

assertThatThrownBy(() -> vaultTemplate.write(SECRET_MOUNT_PATH + SUB_PATH2, body))
.isExactlyInstanceOf(VaultException.class)
.hasMessageContaining(CHECK_AND_SET_REQUIRED_ERROR);

Map<String, Object> options = new HashMap<>();
options.put("cas", "0");

Map<String, Object> versionedBody = new HashMap<>();
versionedBody.put("data", buildCredentialsEntity());
versionedBody.put("options", options);

vaultTemplate.write(SECRET_MOUNT_PATH + SUB_PATH1, versionedBody);
vaultTemplate.write(SECRET_MOUNT_PATH + SUB_PATH2, versionedBody);
}

@Test
public void shouldSuccessfullyUpdateWhenVersionPassedCorrectly() {
String pathKey = SUB_PATH1 + "/success_update_path_key";
int initialVersion = 0;
Versioned<Map<String, String>> body = Versioned.create(buildCredentialsEntity(), Versioned.Version.from(initialVersion));
Versioned.Metadata created = vaultOperations.opsForVersionedKeyValue(SECRET_MOUNT_NAME).put(pathKey, body);
Versioned.Version createdVersion = created.getVersion();

assertThat(createdVersion).isEqualTo(Versioned.Version.from(initialVersion + 1));

Versioned<Map<String, String>> toUpdate = Versioned.create(buildCredentialsEntity(), createdVersion);
vaultOperations.opsForVersionedKeyValue(SECRET_MOUNT_NAME).put(pathKey, toUpdate);

Versioned<Map<String, Object>> secrets = vaultOperations.opsForVersionedKeyValue(SECRET_MOUNT_NAME).get(pathKey);

assertThat(secrets.getData())
.as("check secret")
.containsExactly(entry(SECRET_ENTRY_KEY, SECRET_ENTRY_VALUE));

assertThat(secrets.getVersion())
.isEqualTo(Versioned.Version.from(createdVersion.getVersion() + 1));
}

@EnableAutoConfiguration
@Configuration
static class TestConfiguration {
}

private Map<String, String> buildCredentialsEntity() {
HashMap<String, String> entity = new HashMap<>();
entity.put(SECRET_ENTRY_KEY, SECRET_ENTRY_VALUE);
return entity;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package com.playtika.test.vault;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.vault.VaultException;
import org.springframework.vault.core.VaultOperations;
import org.springframework.vault.core.VaultTemplate;
import org.springframework.vault.support.Versioned;

import java.util.HashMap;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.entry;

@SpringBootTest(properties = {
"embedded.vault.secrets.secret_one=password1",
"embedded.vault.cas-enabled=true"
}, classes = EmbeddedVaultWithCasEnabledTest.TestConfiguration.class)
public class EmbeddedVaultWithCasEnabledTest {
private static final String SECRET_ENTRY_KEY = "cas_key";
private static final String SECRET_ENTRY_VALUE = "cas_val";

private static final String CHECK_AND_SET_REQUIRED_ERROR = "check-and-set parameter required for this call";
private static final String SECRET_MOUNT_NAME = "secret";

@Autowired
private ConfigurableEnvironment environment;

@Autowired
private VaultOperations vaultOperations;

@Autowired
private VaultTemplate vaultTemplate;

@Test
public void propertiesAreAvailable() {
assertThat(environment.getProperty("embedded.vault.host")).isNotEmpty();
assertThat(environment.getProperty("embedded.vault.port")).isNotEmpty();
assertThat(environment.getProperty("embedded.vault.token")).isNotEmpty();
assertThat(environment.getProperty("secret_one")).isNotEmpty();
assertThat(environment.getProperty("embedded.vault.cas-enabled")).isNotEmpty();
}

@Test
public void shouldFailOnWriteWhenCasEnabledButNotPassed() {
assertThatThrownBy(() -> vaultOperations.opsForVersionedKeyValue(SECRET_MOUNT_NAME).put("path_key_cas_not_passed", buildCredentialsEntity()))
.isExactlyInstanceOf(VaultException.class)
.hasMessageContaining("check-and-set parameter required for this call");

HashMap<String, Object> body = new HashMap<>();
body.put("data", buildCredentialsEntity());

assertThatThrownBy(() -> vaultTemplate.write("secret/data/cas_application", body))
.isExactlyInstanceOf(VaultException.class)
.hasMessageContaining(CHECK_AND_SET_REQUIRED_ERROR);

}

@Test
public void shouldFailOnWriteWhenIncorrectVersionPassedWhenCreate() {
Versioned<Map<String, String>> body = Versioned.create(buildCredentialsEntity(), Versioned.Version.from(11));

assertThatThrownBy(() -> vaultOperations.opsForVersionedKeyValue(SECRET_MOUNT_NAME).put("incorrect_cas_version", body))
.isExactlyInstanceOf(VaultException.class)
.hasMessageContaining("check-and-set parameter did not match the current version");
}

@Test
public void shouldFailOnWriteWhenVersionAlreadyExistsOnWriteASecret() {
String pathKey = "when_key_is_already_exists";
Versioned<Map<String, String>> body = Versioned.create(buildCredentialsEntity(), Versioned.Version.from(0));
vaultOperations.opsForVersionedKeyValue("secret").put(pathKey, body);

assertThatThrownBy(() -> vaultOperations.opsForVersionedKeyValue(SECRET_MOUNT_NAME).put(pathKey, body))
.isExactlyInstanceOf(VaultException.class)
.hasMessageContaining("check-and-set parameter did not match the current version");
}

@Test
public void shouldSuccessfullyUpdateWhenVersionPassedCorrectly() {
String pathKey = "success_update_path_key";
int initialVersion = 0;
Versioned<Map<String, String>> body = Versioned.create(buildCredentialsEntity(), Versioned.Version.from(initialVersion));
Versioned.Metadata created = vaultOperations.opsForVersionedKeyValue(SECRET_MOUNT_NAME).put(pathKey, body);
Versioned.Version createdVersion = created.getVersion();

assertThat(createdVersion).isEqualTo(Versioned.Version.from(initialVersion + 1));

Versioned<Map<String, String>> toUpdate = Versioned.create(buildCredentialsEntity(), createdVersion);
vaultOperations.opsForVersionedKeyValue(SECRET_MOUNT_NAME).put(pathKey, toUpdate);

Versioned<Map<String, Object>> secrets = vaultOperations.opsForVersionedKeyValue(SECRET_MOUNT_NAME).get(pathKey);

assertThat(secrets.getData())
.as("check secret")
.containsExactly(entry(SECRET_ENTRY_KEY, SECRET_ENTRY_VALUE));

assertThat(secrets.getVersion())
.isEqualTo(Versioned.Version.from(createdVersion.getVersion() + 1));
}

@EnableAutoConfiguration
@Configuration
static class TestConfiguration {
}

private Map<String, String> buildCredentialsEntity() {
HashMap<String, String> entity = new HashMap<>();
entity.put(SECRET_ENTRY_KEY, SECRET_ENTRY_VALUE);
return entity;
}
}