Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
Expand All @@ -36,7 +38,25 @@
* VaultEnvironmentEncryptor that can decrypt property values prefixed with {vault}
* marker.
*
* <p>
* This class is responsible for decrypting properties in the environment that are
* prefixed with "{vault}". The vault key-value pairs are retrieved from a Vault server
* using the provided {@link VaultKeyValueOperations} template. Properties that start with
* "{vault}" are expected to follow a specific format: "{vault}:key#path" where:
* <ul>
* <li>"key" is the Vault secret path</li>
* <li>"path" is the key within the Vault secret</li>
* </ul>
*
* <p>
* Before retrieving values from Vault, any environment variable placeholders in the
* format ${VAR_NAME} found in the vault reference are replaced with actual environment
* variable values. If an environment variable is not found, the placeholder remains
* unchanged.
* </p>
*
* @author Alexey Zhokhov
* @author Pavel Andrusov
*/
public class VaultEnvironmentEncryptor implements EnvironmentEncryptor {

Expand All @@ -50,6 +70,20 @@ public VaultEnvironmentEncryptor(VaultKeyValueOperations keyValueTemplate) {
this.keyValueTemplate = keyValueTemplate;
}

/**
* Decrypts property values in the provided environment that are prefixed with
* "{vault}". For each such property, this method:
* <ol>
* <li>Extracts the Vault key and path from the property value</li>
* <li>Replaces any environment variable placeholders (${VAR_NAME}) in the vault
* reference with actual environment values</li>
* <li>Retrieves the secret value from Vault using the provided key-value
* template</li>
* <li>Replaces the property value with the decrypted value from Vault</li>
* </ol>
* @param environment the environment containing properties to decrypt
* @return a new Environment object with decrypted values where applicable
*/
@Override
public Environment decrypt(Environment environment) {
Map<String, VaultResponse> loadedVaultKeys = new HashMap<>();
Expand Down Expand Up @@ -86,8 +120,8 @@ public Environment decrypt(Environment environment) {
throw new RuntimeException("Wrong format");
}

String vaultKey = parts[0];
String vaultParamName = parts[1];
String vaultKey = replaceEnvironmentPlaceholders(parts[0]);
String vaultParamName = replaceEnvironmentPlaceholders(parts[1]);

if (!loadedVaultKeys.containsKey(vaultKey)) {
loadedVaultKeys.put(vaultKey, keyValueTemplate.get(vaultKey));
Expand Down Expand Up @@ -125,7 +159,80 @@ else if (logger.isWarnEnabled()) {
return result;
}

public void setPrefixInvalidProperties(boolean prefixInvalidProperties) {
/**
* Replace environment variable placeholders like ${VAR_NAME} with actual values from
* system environment variables.
*
* <p>
* If an environment variable is not found, the placeholder remains unchanged.
* </p>
* @param value the string value that may contain environment variable placeholders
* @return the string with environment variable placeholders replaced by their values,
* or the original string if no placeholders are found
*/
private String replaceEnvironmentPlaceholders(final String value) {
if (value == null) {
logger.debug("Input value is null, returning null");
return null;
}

logger.debug("Processing placeholder replacement for input: %s".formatted(value));

// Pattern to match ${VAR_NAME} format
Pattern pattern = Pattern.compile("\\$\\{([^}]+)}");
Matcher matcher = pattern.matcher(value);

if (!matcher.find()) {
logger.debug("No placeholders found in input string");
return value;
}

// Reset matcher for replacement process
matcher.reset();

StringBuilder result = new StringBuilder();
int lastEnd = 0;
int processedCount = 0;

while (matcher.find()) {
processedCount++;
String variableName = matcher.group(1);
logger.debug("Found placeholder with variable name: %s".formatted(variableName));

// Append the text before the placeholder
result.append(value, lastEnd, matcher.start());

String replacement = System.getenv(variableName);
if (replacement != null) {
// Replace with environment variable value
result.append(replacement);
logger.info(
"Successfully resolved '%s' placeholder from system environment variables. Placeholder replaced."
.formatted(variableName));
}
else {
// If environment variable not found, keep original placeholder
result.append(matcher.group(0));
logger
.warn("Environment variable '%s' not found. Keeping original placeholder.".formatted(variableName));
}

lastEnd = matcher.end();
}

// Append the remaining text after the last placeholder
result.append(value, lastEnd, value.length());

logger.debug("Placeholder replacement completed. Processed %d placeholders.".formatted(processedCount));

return result.toString();
}

/**
* Set whether to prefix invalid properties with "invalid.".
* @param prefixInvalidProperties whether to prefix invalid properties
*/
public void setPrefixInvalidProperties(final boolean prefixInvalidProperties) {
this.prefixInvalidProperties = prefixInvalidProperties;
}

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

/**
* @author Alexey Zhokhov
* @author Pavel Andrusov
*/
public class VaultEnvironmentEncryptorTests {

Expand Down Expand Up @@ -208,6 +209,199 @@ public void shouldMarkAsInvalidPropertyWithWrongFormat2() {
.isEqualTo("<n/a>");
}

@Test
public void shouldResolvePropertyWithEnvironmentVariableInVaultKey() {
// given
String secret = "mysecret";
String vaultKeyWithEnvVar = "accounts/${PATH}/mypay";

VaultKeyValueOperations keyValueTemplate = mock(VaultKeyValueOperations.class);

// Use PATH environment variable which should exist on most systems
String pathValue = System.getenv("PATH");
when(keyValueTemplate.get("accounts/" + pathValue + "/mypay"))
.thenReturn(withVaultResponse("access_key", secret));

VaultEnvironmentEncryptor encryptor = new VaultEnvironmentEncryptor(keyValueTemplate);

// when
Environment environment = new Environment("name", "profile", "label");
environment.add(new PropertySource("a", Collections.<Object, Object>singletonMap(environment.getName(),
"{vault}:" + vaultKeyWithEnvVar + "#access_key")));

// then
assertThat(encryptor.decrypt(environment).getPropertySources().get(0).getSource().get(environment.getName()))
.isEqualTo(secret);
}

@Test
public void shouldResolvePropertyWithEnvironmentVariableInVaultParamName() {
// given
String secret = "mysecret";
String vaultParamWithEnvVar = "${USER}_key";

VaultKeyValueOperations keyValueTemplate = mock(VaultKeyValueOperations.class);

// Use USER environment variable which should exist on most systems
String userValue = System.getenv("USER");
when(keyValueTemplate.get("accounts/mypay")).thenReturn(withVaultResponse(userValue + "_key", secret));

VaultEnvironmentEncryptor encryptor = new VaultEnvironmentEncryptor(keyValueTemplate);

// when
Environment environment = new Environment("name", "profile", "label");
environment.add(new PropertySource("a", Collections.<Object, Object>singletonMap(environment.getName(),
"{vault}:accounts/mypay#" + vaultParamWithEnvVar)));

// then
assertThat(encryptor.decrypt(environment).getPropertySources().get(0).getSource().get(environment.getName()))
.isEqualTo(secret);
}

@Test
public void shouldResolvePropertyWithMultipleEnvironmentVariables() {
// given
String secret = "mysecret";
String vaultKeyWithMultipleEnvVars = "${USER}/accounts/${PATH}/mypay";

VaultKeyValueOperations keyValueTemplate = mock(VaultKeyValueOperations.class);

// Use USER and PATH environment variables which should exist on most systems
String userValue = System.getenv("USER");
String pathValue = System.getenv("PATH");
when(keyValueTemplate.get(userValue + "/accounts/" + pathValue + "/mypay"))
.thenReturn(withVaultResponse("access_key", secret));

VaultEnvironmentEncryptor encryptor = new VaultEnvironmentEncryptor(keyValueTemplate);

// when
Environment environment = new Environment("name", "profile", "label");
environment.add(new PropertySource("a", Collections.<Object, Object>singletonMap(environment.getName(),
"{vault}:" + vaultKeyWithMultipleEnvVars + "#access_key")));

// then
assertThat(encryptor.decrypt(environment).getPropertySources().get(0).getSource().get(environment.getName()))
.isEqualTo(secret);
}

@Test
public void shouldKeepOriginalPlaceholderWhenEnvironmentVariableNotFound() {
// given
String secret = "mysecret";
String vaultKeyWithNonExistentEnvVar = "accounts/${NON_EXISTENT_VAR}/mypay";

VaultKeyValueOperations keyValueTemplate = mock(VaultKeyValueOperations.class);
when(keyValueTemplate.get("accounts/${NON_EXISTENT_VAR}/mypay"))
.thenReturn(withVaultResponse("access_key", secret));

VaultEnvironmentEncryptor encryptor = new VaultEnvironmentEncryptor(keyValueTemplate);

// when
Environment environment = new Environment("name", "profile", "label");
environment.add(new PropertySource("a", Collections.<Object, Object>singletonMap(environment.getName(),
"{vault}:" + vaultKeyWithNonExistentEnvVar + "#access_key")));

// then
assertThat(encryptor.decrypt(environment).getPropertySources().get(0).getSource().get(environment.getName()))
.isEqualTo(secret);
}

@Test
public void shouldHandleMixedExistingAndNonExistingEnvironmentVariables() {
// given
String secret = "mysecret";
String vaultKeyWithMixedEnvVars = "${USER}/accounts/${NON_EXISTENT_VAR}/mypay";

VaultKeyValueOperations keyValueTemplate = mock(VaultKeyValueOperations.class);

// Use USER environment variable which should exist on most systems
String userValue = System.getenv("USER");
when(keyValueTemplate.get(userValue + "/accounts/${NON_EXISTENT_VAR}/mypay"))
.thenReturn(withVaultResponse("access_key", secret));

VaultEnvironmentEncryptor encryptor = new VaultEnvironmentEncryptor(keyValueTemplate);

// when
Environment environment = new Environment("name", "profile", "label");
environment.add(new PropertySource("a", Collections.<Object, Object>singletonMap(environment.getName(),
"{vault}:" + vaultKeyWithMixedEnvVars + "#access_key")));

// then
assertThat(encryptor.decrypt(environment).getPropertySources().get(0).getSource().get(environment.getName()))
.isEqualTo(secret);
}

@Test
public void shouldHandleNullInputGracefully() {
// given
String secret = "mysecret";

VaultKeyValueOperations keyValueTemplate = mock(VaultKeyValueOperations.class);
when(keyValueTemplate.get(null)).thenReturn(withVaultResponse("access_key", secret));

VaultEnvironmentEncryptor encryptor = new VaultEnvironmentEncryptor(keyValueTemplate);

// when
Environment environment = new Environment("name", "profile", "label");
environment.add(new PropertySource("a",
Collections.<Object, Object>singletonMap(environment.getName(), "{vault}:#access_key")));

// then
Environment processedEnvironment = encryptor.decrypt(environment);
assertThat(processedEnvironment.getPropertySources().get(0).getSource().get("invalid." + environment.getName()))
.isEqualTo("<n/a>");
}

@Test
public void shouldHandleEmptyStringInput() {
// given
String secret = "mysecret";

VaultKeyValueOperations keyValueTemplate = mock(VaultKeyValueOperations.class);
when(keyValueTemplate.get("")).thenReturn(withVaultResponse("access_key", secret));

VaultEnvironmentEncryptor encryptor = new VaultEnvironmentEncryptor(keyValueTemplate);

// when
Environment environment = new Environment("name", "profile", "label");
environment.add(new PropertySource("a",
Collections.<Object, Object>singletonMap(environment.getName(), "{vault}:#access_key")));

// then
Environment processedEnvironment = encryptor.decrypt(environment);
assertThat(processedEnvironment.getPropertySources().get(0).getSource().get("invalid." + environment.getName()))
.isEqualTo("<n/a>");
}

@Test
public void shouldHandleMultiplePropertiesWithEnvironmentVariables() {
// given
String secret1 = "secret1";
String secret2 = "secret2";

VaultKeyValueOperations keyValueTemplate = mock(VaultKeyValueOperations.class);
String userValue = System.getenv("USER");
String pathValue = System.getenv("PATH");
when(keyValueTemplate.get("accounts/" + userValue + "/mypay"))
.thenReturn(withVaultResponse("access_key", secret1));
when(keyValueTemplate.get("accounts/" + pathValue + "/mypay"))
.thenReturn(withVaultResponse("access_key", secret2));

VaultEnvironmentEncryptor encryptor = new VaultEnvironmentEncryptor(keyValueTemplate);

// when
Environment environment = new Environment("name", "profile", "label");
Map<Object, Object> properties = new HashMap<>();
properties.put("property1", "{vault}:accounts/${USER}/mypay#access_key");
properties.put("property2", "{vault}:accounts/${PATH}/mypay#access_key");
environment.add(new PropertySource("a", properties));

// then
Environment processedEnvironment = encryptor.decrypt(environment);
assertThat(processedEnvironment.getPropertySources().get(0).getSource().get("property1")).isEqualTo(secret1);
assertThat(processedEnvironment.getPropertySources().get(0).getSource().get("property2")).isEqualTo(secret2);
}

private VaultResponse withVaultResponse(String key, Object value) {
Map<String, Object> responseData = new HashMap<>();
responseData.put(key, value);
Expand Down