Skip to content

Commit

Permalink
feat: support parsing of system environment variables in yaml
Browse files Browse the repository at this point in the history
In order to allow the user to supply environment variables in standard
ways performed in other applications the get_scalar function has been
extended to support defining an environment variable as either the
format $FOO or the format ${FOO}. As this handles some additional
complexity a unit test has been added to cover this new functionality

Signed-off-by: Daniel Wright <danielwright@bitgo.com>
  • Loading branch information
therealdwright committed Sep 5, 2023
1 parent b2374b3 commit 13ab729
Show file tree
Hide file tree
Showing 2 changed files with 139 additions and 9 deletions.
78 changes: 75 additions & 3 deletions unit_tests/falco/test_configuration.cpp
Expand Up @@ -88,16 +88,88 @@ TEST(Configuration, modify_yaml_fields)
{
std::string key = "base_value.subvalue.subvalue2.boolean";
yaml_helper conf;

/* Get original value */
conf.load_from_string(sample_yaml);
ASSERT_EQ(conf.get_scalar<bool>(key, false), true);

/* Modify the original value */
conf.set_scalar<bool>(key, false);
ASSERT_EQ(conf.get_scalar<bool>(key, true), false);

/* Modify it again */
conf.set_scalar<bool>(key, true);
ASSERT_EQ(conf.get_scalar<bool>(key, false), true);
}

TEST(Configuration, configuration_environment_variables)
{
// Set an environment variable for testing purposes
std::string env_var_value = "envVarValue";
std::string env_var_name = "ENV_VAR";
std::string default_value = "default";
setenv(env_var_name.c_str(), env_var_value.c_str(), 1);
yaml_helper conf;

std::string sample_yaml =
"base_value:\n"
" id: $ENV_VAR\n"
" name: '${ENV_VAR}'\n"
" string: my_string\n"
" invalid: $${ENV_VAR}\n"
" invalid_env: $$ENV_VAR\n"
" escaped: \"${ENV_VAR}\"\n"
" subvalue:\n"
" subvalue2:\n"
" boolean: ${UNSED_XX_X_X_VAR}\n"
"base_value_2:\n"
" sample_list:\n"
" - ${ENV_VAR}\n"
" - ' ${ENV_VAR}'\n"
" - $UNSED_XX_X_X_VAR\n";
conf.load_from_string(sample_yaml);

/* Check if the base values are defined */
ASSERT_TRUE(conf.is_defined("base_value"));
ASSERT_TRUE(conf.is_defined("base_value_2"));
ASSERT_FALSE(conf.is_defined("unknown_base_value"));

/* Test fetching of a regular string without any environment variable */
std::string base_value_string = conf.get_scalar<std::string>("base_value.string", default_value);
ASSERT_EQ(base_value_string, "my_string");

/* Test fetching of escaped environment variable format. Should return the string as-is after stripping the leading `$` */
std::string base_value_invalid = conf.get_scalar<std::string>("base_value.invalid", default_value);
ASSERT_EQ(base_value_invalid, "${ENV_VAR}");

/* Test fetching of escaped environment variable ub ubvakud format. Should return the string as-is */
std::string base_value_invalid_env = conf.get_scalar<std::string>("base_value.invalid_env", default_value);
ASSERT_EQ(base_value_invalid_env, "$$ENV_VAR");

/* Test fetching of strings that contain environment variables */
std::string base_value_id = conf.get_scalar<std::string>("base_value.id", default_value);
ASSERT_EQ(base_value_id, "$ENV_VAR"); // Does not follow the `${VAR}` format, so it should be treated as a regular string

std::string base_value_name = conf.get_scalar<std::string>("base_value.name", default_value);
ASSERT_EQ(base_value_name, env_var_value); // Proper environment variable format

std::string base_value_escaped = conf.get_scalar<std::string>("base_value.escaped", default_value);
ASSERT_EQ(base_value_escaped, env_var_value); // Environment variable within quotes

/* Test fetching of an undefined environment variable. Expected to return the default value.*/
std::string unknown_boolean = conf.get_scalar<std::string>("base_value.subvalue.subvalue2.boolean", default_value);
ASSERT_EQ(unknown_boolean, default_value);

/* Test fetching of environment variables from a list */
std::string base_value_2_list_0 = conf.get_scalar<std::string>("base_value_2.sample_list[0]", default_value);
ASSERT_EQ(base_value_2_list_0, env_var_value); // Proper environment variable format

std::string base_value_2_list_1 = conf.get_scalar<std::string>("base_value_2.sample_list[1]", default_value);
ASSERT_EQ(base_value_2_list_1, " ${ENV_VAR}"); // Environment variable preceded by a space, hence treated as a regular string

std::string base_value_2_list_2 = conf.get_scalar<std::string>("base_value_2.sample_list[2]", default_value);
ASSERT_EQ(base_value_2_list_2, "$UNSED_XX_X_X_VAR"); // Does not follow the `${VAR}` format, so should be treated as a regular string

/* Clear the set environment variable after testing */
unsetenv(env_var_name.c_str());
}
70 changes: 64 additions & 6 deletions userspace/falco/yaml_helper.h
Expand Up @@ -72,6 +72,64 @@ class yaml_helper
get_node(node, key);
if(node.IsDefined())
{
std::string value = node.as<std::string>();

// If the value starts with `$$`, check for a subsequent `{...}`
if (value.size() >= 3 && value[0] == '$' && value[1] == '$')
{
// If after stripping the first `$`, the string format is like `${VAR}`, treat it as a plain string and don't resolve.
if (value[2] == '{' && value[value.size() - 1] == '}')
{
value = value.substr(1);
std::stringstream ss(value);
T stripped_value;
if (ss >> stripped_value)
{
return stripped_value;
}
// If the conversion fails for some reason, we should return the default
return default_value;
}
else
{
// If it doesn't have the `${...}` format after `$$`, then it should return the value as-is without stripping.
std::stringstream ss(value);
T preserved_value;
if (ss >> preserved_value)
{
return preserved_value;
}
return default_value;
}
}

// Check if the value is an environment variable reference
if(!value.empty() && value[0] == '$' && value.size() > 2 && value[1] == '{' && value[value.size() - 1] == '}')
{
// Format: ${ENV_VAR_NAME}
std::string env_var = value.substr(2, value.size() - 3);

const char* env_value = std::getenv(env_var.c_str()); // Get the environment variable value
if(env_value)
{
// If the environment variable is found, parse and convert it to the desired type
std::stringstream ss(env_value);
T parsed_value;
if(ss >> parsed_value)
{
return parsed_value;
}
// Parsing failed, return the default value
return default_value;
}
else
{
// Environment variable declared in config not defined on system, return the default value
return default_value;
}
}

// If it's not an environment variable reference, return the value as is
return node.as<T>();
}

Expand Down Expand Up @@ -118,10 +176,10 @@ class yaml_helper
* The provided key string can navigate the document in its
* nested nodes, with arbitrary depth. The key string follows
* this regular language:
*
*
* Key := NodeKey ('.' NodeKey)*
* NodeKey := (any)+ ('[' (integer)+ ']')*
*
*
* Some examples of accepted key strings:
* - NodeName
* - ListValue[3].subvalue
Expand All @@ -146,7 +204,7 @@ class yaml_helper
if (i > 0 && nodeKey.empty() && key[i - 1] != '.')
{
throw std::runtime_error(
"Parsing error: expected '.' character at pos "
"Parsing error: expected '.' character at pos "
+ std::to_string(i - 1));
}
nodeKey += c;
Expand All @@ -157,7 +215,7 @@ class yaml_helper
if (nodeKey.empty())
{
throw std::runtime_error(
"Parsing error: unexpected character at pos "
"Parsing error: unexpected character at pos "
+ std::to_string(i));
}
ret.reset(ret[nodeKey]);
Expand All @@ -181,7 +239,7 @@ class yaml_helper
throw std::runtime_error("Config error at key \"" + key + "\": " + std::string(e.what()));
}
}

template<typename T>
void get_sequence_from_node(T& ret, const YAML::Node& node) const
{
Expand Down Expand Up @@ -250,7 +308,7 @@ namespace YAML {
default:
break;
}

return true;
}
};
Expand Down

0 comments on commit 13ab729

Please sign in to comment.