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

Added the possibility of configura a fallback file #18

Merged
merged 15 commits into from
Sep 20, 2022
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
24 changes: 24 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,30 @@
<version>${mock.server.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.1.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>4.6.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>uk.org.webcompere</groupId>
<artifactId>system-stubs-jupiter</artifactId>
<version>1.1.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mock-server</groupId>
<artifactId>mockserver-client-java</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.avioconsulting.mule.vault.provider.internal.error.exception.SecretNotFoundException;
import com.avioconsulting.mule.vault.provider.internal.error.exception.UnsetVariableException;
import com.avioconsulting.mule.vault.provider.internal.error.exception.VaultAccessException;
import com.avioconsulting.mule.vault.provider.internal.extension.VaultPropertyPath;
import com.bettercloud.vault.Vault;
import com.bettercloud.vault.VaultException;
import org.mule.runtime.api.exception.DefaultMuleException;
Expand All @@ -11,60 +12,76 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.FileInputStream;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Provider to read Vault properties from the Vault server.
* Provider to read Vault properties from the Vault server or fallback file.
*/
public class VaultConfigurationPropertiesProvider implements ConfigurationPropertiesProvider {

private final static Logger logger = LoggerFactory.getLogger(VaultConfigurationPropertiesProvider.class);

private final static String VAULT_PROPERTIES_PREFIX = "vault::";
private final static Pattern VAULT_PATTERN = Pattern.compile(VAULT_PROPERTIES_PREFIX + "([^.}]*).(.*)");
private final static Pattern ENV_PATTERN = Pattern.compile("\\$\\[([^\\]]*)\\]");
private static final Logger logger = LoggerFactory.getLogger(VaultConfigurationPropertiesProvider.class);
private static final String VAULT_PROPERTIES_PREFIX = "vault::";
private static final Pattern VAULT_PATTERN = Pattern.compile(VAULT_PROPERTIES_PREFIX + "([^\\.]*)\\.([^}]*)");
private static final Pattern ENV_PATTERN = Pattern.compile("\\$\\[([^\\]]*)\\]");

private final Vault vault;

private final boolean isLocalMode;

Map<String, Map<String,String>> cachedData;

/**
* Constructs a VaultConfigurationPropertiesProvider. Vault must not be null.
* @param vault
* If isLocalMode is true, it will use the fallback file which contain secretPath and secrets.
* All Vault connections will be disabled when isLocalMode be true.
*
* @param vault vault object which contains secrets to pull from.
* @param isLocalMode determines whether local fileback mode is enable or not.
* @param localPropertiesFile local properties file name.
*/
public VaultConfigurationPropertiesProvider(final Vault vault) {
public VaultConfigurationPropertiesProvider(final Vault vault, final Boolean isLocalMode, final String localPropertiesFile) {
this.vault = vault;
cachedData = new HashMap<>();
this.isLocalMode = isLocalMode;
if (isLocalMode) {
evaluateLocalProperitesConfig(localPropertiesFile);
}
}

/**
* Retrieves the property value from Vault. It stores the retrieved path in a Map so a Dynamic Secrets can be used.
*
* @param path the path to the secret
* @param property the property to retrieve from the secret
* @return the value of the property or null if the property is not found
* @param path the path to the secret.
* @param property the property to retrieve from the secret.
* @return the value of the property or null if the property is not found.
*/
private String getProperty(String path, String property) throws DefaultMuleException {



private String getProperty(String path, String property) throws SecretNotFoundException, VaultAccessException, DefaultMuleException {
try {
Map<String, String> data = null;
if (cachedData.containsKey(path)) {
logger.trace("Getting data from cache");
data = cachedData.get(path);
} else {
} else if (!isLocalMode) {
logger.trace("Getting data from Vault");
data = vault.logical().read(path).getData();
// TODO: Does the driver ever return null? Or does it throw an exception? It returns a null if there is no data stored in the secret
cachedData.put(path, data);
} else {
// path is not in the cache and isLocalMode is true, so notify that the property is not in the local properties file
throw new SecretNotFoundException(String.format("No property found for %s.%s in the local properties file", path, property));
}

if (data != null) {
if (data != null && data.containsKey(property)) {
return data.get(property);
} else {
throw new SecretNotFoundException(String.format("No value found for %s.%s", path, property));
}

} catch (VaultException ve) {
Expand All @@ -76,36 +93,24 @@ private String getProperty(String path, String property) throws DefaultMuleExcep
logger.error("Error getting data from Vault", ve);
throw new DefaultMuleException("Unknown Vault exception", ve);
}

}

return null;
}

/**
* Get a configuration property value from Vault.
* Get a configuration property value from Vault or a local properties file.
*
* @param configurationAttributeKey the key to lookup
* @return the String value of the property
* @param configurationAttributeKey the key to lookup.
* @return an {@link Optional} containing the {@link ConfigurationProperty} which holds
* the value for the given key at the given secret path.
*/
@Override
public Optional<ConfigurationProperty> getConfigurationProperty(String configurationAttributeKey) {

Optional optionalValue = Optional.empty();
if (configurationAttributeKey.startsWith(VAULT_PROPERTIES_PREFIX)) {
Matcher matcher = VAULT_PATTERN.matcher(configurationAttributeKey);
if (matcher.find()) {

final String effectiveKey = configurationAttributeKey.substring(VAULT_PROPERTIES_PREFIX.length());

// The Vault path is everything after the prefix and before the first period
final String vaultPath = matcher.group(1);

// The secret key is everything after the first period
final String secretKey = matcher.group(2);

VaultPropertyPath path = parsePropertyPath(configurationAttributeKey);
if (path != null) {
try {
final String value = getProperty(expandedValue(vaultPath), expandedValue(secretKey));

final String value = getProperty(path.getSecretPath(), path.getKey());
if (value != null) {
return Optional.of(new ConfigurationProperty() {

Expand All @@ -121,34 +126,54 @@ public Object getRawValue() {

@Override
public String getKey() {
return effectiveKey;
return String.format("%s.%s", path.getSecretPath(), path.getKey());
}
});
}
} catch (Exception e) {
logger.error("Property was not found", e);
return Optional.empty();
}

return Optional.empty();

}
}
return Optional.empty();
return optionalValue;
}

@Override
public String getDescription() {
return "Vault properties provider";
}

/**
* Parse the configurationAttributeKey to determine if the key provided should be retrieved from Vault.
* The configurationAttributeKey must match VAULT_PATTERN.
* Not all configurationAttributeKeys are meant for the Vault Properties Provider and they must be ignored.
*
* @param configurationAttributeKey a String representing the secret path and key that should be parsed.
* @return a {@link VaultPropertyPath} with the Vault Secret Path and Key to retrieve or null if it is not a Vault property.
*/
private VaultPropertyPath parsePropertyPath(String configurationAttributeKey) {

Matcher matcher = VAULT_PATTERN.matcher(configurationAttributeKey);
if (matcher.find()) {
// The Vault path is everything after the prefix and before the first period
final String secretPath = matcher.group(1);

// The key is everything after the first period
final String key = matcher.group(2);

return new VaultPropertyPath(expandedValue(secretPath), expandedValue(key));
}

return null;
}

/**
* Retrieve values from the environment when the pattern \$\[[^\]]*\] is used in a property value and replace the pattern
* with the value. Example matches: $[ENV] or $[environment]
* with the value. Example matches: $[ENV] or $[environment].
*
* @param value the text to search for the pattern and replace with values
* @return the inserted text with environment variables looked up
* @throws UnsetVariableException when the environment variable is not set
* @param value the text to search for the pattern and replace with values.
* @return the inserted text with environment variables looked up.
* @throws UnsetVariableException when the environment variable is not set.
*/
private String expandedValue(final String value) {
String result = value;
Expand All @@ -171,4 +196,47 @@ private String expandedValue(final String value) {

return result;
}

/**
* Read a properties file from localPropertiesFile and load the local cache with its values.
* @param localPropertiesFile path to a properties file located on the classpath.
*/
private void evaluateLocalProperitesConfig(String localPropertiesFile){
if(localPropertiesFile==null || localPropertiesFile.isEmpty())
return;
try {
URL resourceUrl = this.getClass().getClassLoader().getResource(localPropertiesFile);
if(resourceUrl==null) return;
Properties appProps = new Properties();
appProps.load(new FileInputStream(resourceUrl.getPath()));
loadCacheFromProperties(appProps);
} catch(Exception e) {
logger.error(String.format("Error occurred while loading the local properties file from %s", localPropertiesFile), e);
}
}

/**
* Populate the cache using the properties provided. Logs warnings if the properties to not match the expected format.
* Expected format: path/to/secret/engine.key.
*
* Secret paths and the key cannot have periods in them.
*
* @param properties A {@link Properties} object to load the cache with.
*/
private void loadCacheFromProperties(Properties properties){
if(properties == null || properties.isEmpty()) return;

properties.entrySet().forEach(entry ->{
String keyS = entry.getKey().toString();
String[] pathElements = keyS.split("\\.");
if(pathElements.length != 2){
logger.warn(String.format("Invalid property secret path in local properties file (%s). Property must be in this format: path/to/secret/engine.key", keyS));
} else {
if(cachedData.containsKey(pathElements[0]))
cachedData.get(pathElements[0]).put(pathElements[1], entry.getValue().toString());
else
cachedData.put(pathElements[0], new HashMap<String,String>(){ { put(pathElements[1], entry.getValue().toString()); } });
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.slf4j.LoggerFactory;

import com.avioconsulting.mule.vault.provider.internal.connection.VaultConnection;
import com.avioconsulting.mule.vault.provider.internal.connection.provider.AbstractConnectionProvider;
import com.avioconsulting.mule.vault.provider.internal.connection.provider.AppRoleConnectionProvider;
import com.avioconsulting.mule.vault.provider.internal.connection.provider.Ec2ConnectionProvider;
import com.avioconsulting.mule.vault.provider.internal.connection.provider.IamConnectionProvider;
Expand Down Expand Up @@ -41,7 +42,10 @@ public ComponentIdentifier getSupportedComponentIdentifier() {
public ConfigurationPropertiesProvider createProvider(final ConfigurationParameters parameters,
ResourceProvider externalResourceProvider) {
try {
return new VaultConfigurationPropertiesProvider(getVault(parameters));
AbstractConnectionProvider vaultConnectionProvider = getVaultConnectionProvider(parameters);
return new VaultConfigurationPropertiesProvider(vaultConnectionProvider.connect().getVault(),
vaultConnectionProvider.isLocalMode(),
vaultConnectionProvider.getLocalPropertiesFile());
} catch (ConnectionException ce) {
logger.error("Error connecting to Vault", ce);
return null;
Expand All @@ -54,7 +58,7 @@ public ConfigurationPropertiesProvider createProvider(final ConfigurationParamet
* @param parameters The parameters read from the Mule config file
* @return a fully configured {@link Vault} object
*/
private Vault getVault(ConfigurationParameters parameters) throws ConnectionException {
private AbstractConnectionProvider getVaultConnectionProvider(ConfigurationParameters parameters) throws ConnectionException {
if (parameters.getComplexConfigurationParameters().size() > 1) {
logger.warn("Multiple Vault Properties Provider configurations have been found");
}
Expand Down Expand Up @@ -89,12 +93,11 @@ private Vault getVault(ConfigurationParameters parameters) throws ConnectionExce
}

if (connectionProvider != null) {
return connectionProvider.connect().getVault();
return (AbstractConnectionProvider) connectionProvider;
} else {
logger.warn("No Vault Properties Provider configurations found");
return null;
}

}

}
Loading