diff --git a/rcl/CMakeLists.txt b/rcl/CMakeLists.txt index c8ee07353..cee1035e8 100644 --- a/rcl/CMakeLists.txt +++ b/rcl/CMakeLists.txt @@ -9,6 +9,7 @@ find_package(rcutils REQUIRED) find_package(rmw REQUIRED) find_package(rmw_implementation REQUIRED) find_package(rosidl_generator_c REQUIRED) +find_package(tinydir_vendor REQUIRED) include_directories(include) @@ -54,6 +55,7 @@ set(${PROJECT_NAME}_sources src/rcl/timer.c src/rcl/validate_topic_name.c src/rcl/wait.c + src/rcl/security_directory.c ) add_library(${PROJECT_NAME} ${${PROJECT_NAME}_sources}) @@ -65,6 +67,7 @@ ament_target_dependencies(${PROJECT_NAME} "rcutils" "rosidl_generator_c" ${RCL_LOGGING_IMPL} + "tinydir_vendor" ) # Causes the visibility macros to use dllexport rather than dllimport, diff --git a/rcl/include/rcl/security_directory.h b/rcl/include/rcl/security_directory.h new file mode 100644 index 000000000..c9292a613 --- /dev/null +++ b/rcl/include/rcl/security_directory.h @@ -0,0 +1,66 @@ +// Copyright 2018 Open Source Robotics Foundation, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef RCL__SECURITY_DIRECTORY_H_ +#define RCL__SECURITY_DIRECTORY_H_ + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include "rcl/allocator.h" +#include "rcl/visibility_control.h" + +#ifndef ROS_SECURITY_NODE_DIRECTORY_VAR_NAME + #define ROS_SECURITY_NODE_DIRECTORY_VAR_NAME "ROS_SECURITY_NODE_DIRECTORY" +#endif + +#ifndef ROS_SECURITY_ROOT_DIRECTORY_VAR_NAME + #define ROS_SECURITY_ROOT_DIRECTORY_VAR_NAME "ROS_SECURITY_ROOT_DIRECTORY" +#endif + +#ifndef ROS_SECURITY_LOOKUP_TYPE_VAR_NAME + #define ROS_SECURITY_LOOKUP_TYPE_VAR_NAME "ROS_SECURITY_LOOKUP_TYPE" +#endif + +/// Return the secure root directory associated with a node given its validated name and namespace. +/** + * E.g. for a node named "c" in namespace "/a/b", the secure root path will be + * "a/b/c", where the delimiter "/" is native for target file system (e.g. "\\" for _WIN32). + * If no exact match is found for the node name, a best match would be used instead + * (by performing longest-prefix matching). + * + * However, this expansion can be overridden by setting the secure node directory environment + * variable, allowing users to explicitly specify the exact secure root directory to be utilized. + * Such an override is useful for where the FQN of a node is non-deterministic before runtime, + * or when testing and using additional tools that may not otherwise be easily provisioned. + * + * \param[in] node_name validated node name (a single token) + * \param[in] node_namespace validated, absolute namespace (starting with "/") + * \param[in] allocator the allocator to use for allocation + * \returns machine specific (absolute) node secure root path or NULL on failure + */ +RCL_PUBLIC +const char * rcl_get_secure_root( + const char * node_name, + const char * node_namespace, + const rcl_allocator_t * allocator +); + +#ifdef __cplusplus +} +#endif + +#endif // RCL__SECURITY_DIRECTORY_H_ diff --git a/rcl/package.xml b/rcl/package.xml index e9371d831..b4d7b9917 100644 --- a/rcl/package.xml +++ b/rcl/package.xml @@ -16,10 +16,12 @@ rcl_interfaces rcutils rosidl_generator_c + tinydir_vendor rcl_interfaces rcutils rosidl_generator_c + tinydir_vendor rcl_interfaces ament_cmake diff --git a/rcl/src/rcl/node.c b/rcl/src/rcl/node.c index 536cddbcc..4973a9b5b 100644 --- a/rcl/src/rcl/node.c +++ b/rcl/src/rcl/node.c @@ -29,6 +29,7 @@ extern "C" #include "rcl/logging_rosout.h" #include "rcl/rcl.h" #include "rcl/remap.h" +#include "rcl/security_directory.h" #include "rcutils/filesystem.h" #include "rcutils/find.h" #include "rcutils/format_string.h" @@ -46,9 +47,8 @@ extern "C" #include "./common.h" #include "./context_impl.h" +#include "tinydir/tinydir.h" -#define ROS_SECURITY_NODE_DIRECTORY_VAR_NAME "ROS_SECURITY_NODE_DIRECTORY" -#define ROS_SECURITY_ROOT_DIRECTORY_VAR_NAME "ROS_SECURITY_ROOT_DIRECTORY" #define ROS_SECURITY_STRATEGY_VAR_NAME "ROS_SECURITY_STRATEGY" #define ROS_SECURITY_ENABLE_VAR_NAME "ROS_SECURITY_ENABLE" @@ -101,86 +101,6 @@ const char * rcl_create_node_logger_name( return node_logger_name; } -/// Return the secure root directory associated with a node given its validated name and namespace. -/** - * E.g. for a node named "c" in namespace "/a/b", the secure root path will be - * "a/b/c", where the delimiter "/" is native for target file system (e.g. "\\" for _WIN32). - * However, this expansion can be overridden by setting the secure node directory environment - * variable, allowing users to explicitly specify the exact secure root directory to be utilized. - * Such an override is useful for where the FQN of a node is non-deterministic before runtime, - * or when testing and using additional tools that may not otherwise not be easily provisioned. - * - * \param[in] node_name validated node name (a single token) - * \param[in] node_namespace validated, absolute namespace (starting with "/") - * \param[in] allocator the allocator to use for allocation - * \returns machine specific (absolute) node secure root path or NULL on failure - */ -const char * rcl_get_secure_root( - const char * node_name, - const char * node_namespace, - const rcl_allocator_t * allocator) -{ - bool ros_secure_node_override = true; - const char * ros_secure_root_env = NULL; - if (NULL == node_name) { - return NULL; - } - if (rcutils_get_env(ROS_SECURITY_NODE_DIRECTORY_VAR_NAME, &ros_secure_root_env)) { - return NULL; - } - if (!ros_secure_root_env) { - return NULL; - } - size_t ros_secure_root_size = strlen(ros_secure_root_env); - if (!ros_secure_root_size) { - // check root directory if node directory environment variable is empty - if (rcutils_get_env(ROS_SECURITY_ROOT_DIRECTORY_VAR_NAME, &ros_secure_root_env)) { - return NULL; - } - if (!ros_secure_root_env) { - return NULL; - } - ros_secure_root_size = strlen(ros_secure_root_env); - if (!ros_secure_root_size) { - return NULL; // environment variable was empty - } else { - ros_secure_node_override = false; - } - } - char * node_secure_root = NULL; - if (ros_secure_node_override) { - node_secure_root = - (char *)allocator->allocate(ros_secure_root_size + 1, allocator->state); - memcpy(node_secure_root, ros_secure_root_env, ros_secure_root_size + 1); - // TODO(ros2team): This make an assumption on the value and length of the root namespace. - // This should likely come from another (rcl/rmw?) function for reuse. - // If the namespace is the root namespace ("/"), the secure root is just the node name. - } else if (strlen(node_namespace) == 1) { - node_secure_root = rcutils_join_path(ros_secure_root_env, node_name, *allocator); - } else { - char * node_fqn = NULL; - char * node_root_path = NULL; - // Combine node namespace with node name - // TODO(ros2team): remove the hard-coded value of the root namespace. - node_fqn = rcutils_format_string(*allocator, "%s%s%s", node_namespace, "/", node_name); - // Get native path, ignore the leading forward slash. - // TODO(ros2team): remove the hard-coded length, use the length of the root namespace instead. - node_root_path = rcutils_to_native_path(node_fqn + 1, *allocator); - node_secure_root = rcutils_join_path(ros_secure_root_env, node_root_path, *allocator); - allocator->deallocate(node_fqn, allocator->state); - allocator->deallocate(node_root_path, allocator->state); - } - // Check node_secure_root is not NULL before checking directory - if (NULL == node_secure_root) { - allocator->deallocate(node_secure_root, allocator->state); - return NULL; - } else if (!rcutils_is_directory(node_secure_root)) { - allocator->deallocate(node_secure_root, allocator->state); - return NULL; - } - return node_secure_root; -} - rcl_node_t rcl_get_zero_initialized_node() { @@ -385,15 +305,10 @@ rcl_node_init( // File discovery magic here const char * node_secure_root = rcl_get_secure_root(name, local_namespace_, allocator); if (node_secure_root) { + RCUTILS_LOG_INFO_NAMED(ROS_PACKAGE_NAME, "Found security directory: %s", node_secure_root); node_security_options.security_root_path = node_secure_root; } else { if (RMW_SECURITY_ENFORCEMENT_ENFORCE == node_security_options.enforce_security) { - RCL_SET_ERROR_MSG( - "SECURITY ERROR: unable to find a folder matching the node name in the " - RCUTILS_STRINGIFY(ROS_SECURITY_NODE_DIRECTORY_VAR_NAME) - " or " - RCUTILS_STRINGIFY(ROS_SECURITY_ROOT_DIRECTORY_VAR_NAME) - " directories while the requested security strategy requires it"); ret = RCL_RET_ERROR; goto cleanup; } diff --git a/rcl/src/rcl/security_directory.c b/rcl/src/rcl/security_directory.c new file mode 100644 index 000000000..a30baac10 --- /dev/null +++ b/rcl/src/rcl/security_directory.c @@ -0,0 +1,249 @@ +// Copyright 2018 Open Source Robotics Foundation, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "rcl/security_directory.h" +#include "tinydir/tinydir.h" +#include "rcutils/filesystem.h" +#include "rcutils/get_env.h" +#include "rcutils/format_string.h" +#include "rcl/error_handling.h" + +/** + * A security lookup function takes in the node's name, namespace, a security root directory and an allocator; + * It returns the relevant information required to load the security credentials, + * which is currently a path to a directory on the filesystem containing DDS Security permission files. + */ +typedef char * (* security_lookup_fn_t) ( + const char * node_name, + const char * node_namespace, + const char * ros_secure_root_env, + const rcl_allocator_t * allocator +); + +char * exact_match_lookup( + const char * node_name, + const char * node_namespace, + const char * ros_secure_root_env, + const rcl_allocator_t * allocator +); + +char * prefix_match_lookup( + const char * node_name, + const char * node_namespace, + const char * ros_secure_root_env, + const rcl_allocator_t * allocator +); + +security_lookup_fn_t g_security_lookup_fns[] = { + NULL, + exact_match_lookup, + prefix_match_lookup, +}; + +typedef enum ros_security_lookup_type_e +{ + ROS_SECURITY_LOOKUP_NODE_OVERRIDE = 0, + ROS_SECURITY_LOOKUP_MATCH_EXACT = 1, + ROS_SECURITY_LOOKUP_MATCH_PREFIX = 2, +} ros_security_lookup_type_t; + +char * g_security_lookup_type_strings[] = { + "NODE_OVERRIDE", + "MATCH_EXACT", + "MATCH_PREFIX" +}; + +/// Return the directory whose name most closely matches node_name (longest-prefix match), +/// scanning under base_dir. +/** + * By using a prefix match, a node named e.g. "my_node_123" will be able to load and use the + * directory "my_node" if no better match exists. + * \param[in] base_dir + * \param[in] node_name + * \param[out] matched_name must be a valid memory address allocated with at least + * _TINYDIR_FILENAME_MAX characters. + * \return true if a match was found + */ +static bool get_best_matching_directory( + const char * base_dir, + const char * node_name, + char * matched_name) +{ + size_t max_match_length = 0; + tinydir_dir dir; + if (NULL == base_dir || NULL == node_name || NULL == matched_name) { + return false; + } + if (-1 == tinydir_open(&dir, base_dir)) { + return false; + } + while (dir.has_next) { + tinydir_file file; + if (-1 == tinydir_readfile(&dir, &file)) { + goto cleanup; + } + if (file.is_dir) { + size_t matched_name_length = strnlen(file.name, sizeof(file.name) - 1); + if (0 == + strncmp(file.name, node_name, + matched_name_length) && matched_name_length > max_match_length) + { + max_match_length = matched_name_length; + memcpy(matched_name, file.name, max_match_length); + } + } + if (-1 == tinydir_next(&dir)) { + goto cleanup; + } + } +cleanup: + tinydir_close(&dir); + return max_match_length > 0; +} + +char * exact_match_lookup( + const char * node_name, + const char * node_namespace, + const char * ros_secure_root_env, + const rcl_allocator_t * allocator) +{ + // Perform an exact match for the node's name in directory /. + char * node_secure_root = NULL; + // "/" case when root namespace is explicitly passed in + if (1 == strlen(node_namespace)) { + node_secure_root = rcutils_join_path(ros_secure_root_env, node_name, *allocator); + } else { + char * node_fqn = NULL; + char * node_root_path = NULL; + // Combine node namespace with node name + // TODO(ros2team): remove the hard-coded value of the root namespace + node_fqn = rcutils_format_string(*allocator, "%s%s%s", node_namespace, "/", node_name); + // Get native path, ignore the leading forward slash + // TODO(ros2team): remove the hard-coded length, use the length of the root namespace instead + node_root_path = rcutils_to_native_path(node_fqn + 1, *allocator); + node_secure_root = rcutils_join_path(ros_secure_root_env, node_root_path, *allocator); + allocator->deallocate(node_fqn, allocator->state); + allocator->deallocate(node_root_path, allocator->state); + } + return node_secure_root; +} + +char * prefix_match_lookup( + const char * node_name, + const char * node_namespace, + const char * ros_secure_root_env, + const rcl_allocator_t * allocator) +{ + // Perform longest prefix match for the node's name in directory /. + char * node_secure_root = NULL; + char matched_dir[_TINYDIR_FILENAME_MAX] = {0}; + char * base_lookup_dir = NULL; + if (strlen(node_namespace) == 1) { + base_lookup_dir = (char *) ros_secure_root_env; + } else { + // TODO(ros2team): remove the hard-coded length, use the length of the root namespace instead. + base_lookup_dir = rcutils_join_path(ros_secure_root_env, node_namespace + 1, *allocator); + } + if (get_best_matching_directory(base_lookup_dir, node_name, matched_dir)) { + node_secure_root = rcutils_join_path(base_lookup_dir, matched_dir, *allocator); + } + if (base_lookup_dir != ros_secure_root_env && NULL != base_lookup_dir) { + allocator->deallocate(base_lookup_dir, allocator->state); + } + return node_secure_root; +} + +const char * rcl_get_secure_root( + const char * node_name, + const char * node_namespace, + const rcl_allocator_t * allocator) +{ + bool ros_secure_node_override = true; + + // find out if either of the configuration environment variables are set + const char * env_buf = NULL; + if (NULL == node_name) { + return NULL; + } + if (rcutils_get_env(ROS_SECURITY_NODE_DIRECTORY_VAR_NAME, &env_buf)) { + return NULL; + } + if (!env_buf) { + return NULL; + } + size_t ros_secure_root_size = strlen(env_buf); + if (!ros_secure_root_size) { + // check root directory if node directory environment variable is empty + if (rcutils_get_env(ROS_SECURITY_ROOT_DIRECTORY_VAR_NAME, &env_buf)) { + return NULL; + } + if (!env_buf) { + return NULL; + } + ros_secure_root_size = strlen(env_buf); + if (!ros_secure_root_size) { + return NULL; // environment variable was empty + } else { + ros_secure_node_override = false; + } + } + + // found a usable environment variable, copy into our memory before overwriting with next lookup + char * ros_secure_root_env = + (char *)allocator->allocate(ros_secure_root_size + 1, allocator->state); + memcpy(ros_secure_root_env, env_buf, ros_secure_root_size + 1); + // TODO(ros2team): This make an assumption on the value and length of the root namespace. + // This should likely come from another (rcl/rmw?) function for reuse. + // If the namespace is the root namespace ("/"), the secure root is just the node name. + + char * lookup_strategy = NULL; + char * node_secure_root = NULL; + if (ros_secure_node_override) { + node_secure_root = ros_secure_root_env; + lookup_strategy = g_security_lookup_type_strings[ROS_SECURITY_LOOKUP_NODE_OVERRIDE]; + + } else { + // Check which lookup method to use and invoke the relevant function. + const char * ros_security_lookup_type = NULL; + if (rcutils_get_env(ROS_SECURITY_LOOKUP_TYPE_VAR_NAME, &ros_security_lookup_type)) { + return NULL; + } + if (0 == strcmp(ros_security_lookup_type, + g_security_lookup_type_strings[ROS_SECURITY_LOOKUP_MATCH_PREFIX])) + { + node_secure_root = g_security_lookup_fns[ROS_SECURITY_LOOKUP_MATCH_PREFIX] + (node_name, node_namespace, ros_secure_root_env, allocator); + lookup_strategy = g_security_lookup_type_strings[ROS_SECURITY_LOOKUP_MATCH_PREFIX]; + } else { /* Default is MATCH_EXACT */ + node_secure_root = g_security_lookup_fns[ROS_SECURITY_LOOKUP_MATCH_EXACT] + (node_name, node_namespace, ros_secure_root_env, allocator); + lookup_strategy = g_security_lookup_type_strings[ROS_SECURITY_LOOKUP_MATCH_EXACT]; + } + } + // Check node_secure_root is not NULL before checking directory + if (NULL == node_secure_root) { + RCL_SET_ERROR_MSG_WITH_FORMAT_STRING( + "SECURITY ERROR: unable to find a folder matching the node name in %s%s." + "Lookup strategy: %s", + ros_secure_root_env, node_namespace, lookup_strategy); + } else if (!rcutils_is_directory(node_secure_root)) { + RCL_SET_ERROR_MSG_WITH_FORMAT_STRING( + "SECURITY ERROR: directory %s does not exist. Lookup strategy: %s", + node_secure_root, lookup_strategy); + allocator->deallocate(node_secure_root, allocator->state); + } else { + return node_secure_root; + } + return NULL; +} diff --git a/rcl/test/CMakeLists.txt b/rcl/test/CMakeLists.txt index 5eed488d0..6e17bd7ce 100644 --- a/rcl/test/CMakeLists.txt +++ b/rcl/test/CMakeLists.txt @@ -15,6 +15,8 @@ include(cmake/rcl_add_custom_gtest.cmake) include(cmake/rcl_add_custom_launch_test.cmake) set(extra_lib_dirs "${rcl_lib_dir}") +set(test_resources_dir_name "resources") +add_definitions(-DTEST_RESOURCES_DIRECTORY="${CMAKE_CURRENT_BINARY_DIR}/${test_resources_dir_name}") # finding gtest once in the highest scope # prevents finding it repeatedly in each local scope @@ -298,3 +300,13 @@ rcl_add_custom_gtest(test_timer${target_suffix} APPEND_LIBRARY_DIRS ${extra_lib_dirs} LIBRARIES ${PROJECT_NAME} ) + +rcl_add_custom_gtest(test_security_directory + SRCS rcl/test_security_directory.cpp + APPEND_LIBRARY_DIRS ${extra_lib_dirs} + LIBRARIES ${PROJECT_NAME} +) + +# Install test resources +install(DIRECTORY ${test_resources_dir_name} + DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) \ No newline at end of file diff --git a/rcl/test/rcl/test_security_directory.cpp b/rcl/test/rcl/test_security_directory.cpp new file mode 100644 index 000000000..3b054a317 --- /dev/null +++ b/rcl/test/rcl/test_security_directory.cpp @@ -0,0 +1,164 @@ +// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include +#include "rcl/security_directory.h" +#include "rcutils/filesystem.h" + +#define ROOT_NAMESPACE "/" +#define TEST_SECURITY_DIRECTORY_RESOURCES_DIR_NAME "test_security_directory" +#define TEST_NODE_NAME "dummy_node" +#define TEST_NODE_NAMESPACE ROOT_NAMESPACE TEST_SECURITY_DIRECTORY_RESOURCES_DIR_NAME + +char g_envstring[512] = {0}; + +static int putenv_wrapper(const char * env_var) +{ +#ifdef _WIN32 + return _putenv(env_var); +#else + return putenv(reinterpret_cast(const_cast(env_var))); +#endif +} + +static int unsetenv_wrapper(const char * var_name) +{ +#ifdef _WIN32 + // On windows, putenv("VAR=") deletes VAR from environment + std::string var(var_name); + var += "="; + return _putenv(var.c_str()); +#else + return unsetenv(var_name); +#endif +} + +class TestGetSecureRoot : public ::testing::Test +{ +protected: + void SetUp() override + { + // Always make sure the variable we set is unset at the beginning of a test + unsetenv_wrapper(ROS_SECURITY_ROOT_DIRECTORY_VAR_NAME); + unsetenv_wrapper(ROS_SECURITY_NODE_DIRECTORY_VAR_NAME); + unsetenv_wrapper(ROS_SECURITY_LOOKUP_TYPE_VAR_NAME); + } +}; + +TEST_F(TestGetSecureRoot, failureScenarios) { + rcl_allocator_t allocator = rcl_get_default_allocator(); + ASSERT_EQ(rcl_get_secure_root(TEST_NODE_NAME, TEST_NODE_NAMESPACE, &allocator), + (char *) NULL); + + putenv_wrapper(ROS_SECURITY_ROOT_DIRECTORY_VAR_NAME "=" TEST_RESOURCES_DIRECTORY); + + /* Security directory is set, but there's no matching directory */ + /// Wrong namespace + ASSERT_EQ(rcl_get_secure_root(TEST_NODE_NAME, "/some_other_namespace", &allocator), + (char *) NULL); + /// Wrong node name + ASSERT_EQ(rcl_get_secure_root("not_" TEST_NODE_NAME, TEST_NODE_NAMESPACE, &allocator), + (char *) NULL); +} + +TEST_F(TestGetSecureRoot, successScenarios) { + rcl_allocator_t allocator = rcl_get_default_allocator(); + putenv_wrapper(ROS_SECURITY_ROOT_DIRECTORY_VAR_NAME "=" TEST_RESOURCES_DIRECTORY); + /* -------------------------- + * Namespace : Custom (local) + * Match type : Exact + * -------------------------- + * Root: ${CMAKE_BINARY_DIR}/tests/resources + * Namespace: /test_security_directory + * Node: dummy_node + */ + std::string secure_root = rcl_get_secure_root(TEST_NODE_NAME, TEST_NODE_NAMESPACE, &allocator); + ASSERT_STREQ(TEST_NODE_NAME, + secure_root.substr(secure_root.size() - (sizeof(TEST_NODE_NAME) - 1)).c_str()); + + /* -------------------------- + * Namespace : Custom (local) + * Match type : Prefix + * -------------------------- + * Root: ${CMAKE_BINARY_DIR}/tests/resources + * Namespace: /test_security_directory + * Node: dummy_node_and_some_suffix_added */ + ASSERT_STRNE(rcl_get_secure_root(TEST_NODE_NAME "_and_some_suffix_added", TEST_NODE_NAMESPACE, + &allocator), + secure_root.c_str()); + putenv_wrapper(ROS_SECURITY_LOOKUP_TYPE_VAR_NAME "=MATCH_PREFIX"); + ASSERT_STREQ(rcl_get_secure_root(TEST_NODE_NAME "_and_some_suffix_added", TEST_NODE_NAMESPACE, + &allocator), + secure_root.c_str()); + + /* Include the namespace as part of the root security directory and test root namespace */ + char * base_lookup_dir_fqn = rcutils_join_path(TEST_RESOURCES_DIRECTORY, + TEST_SECURITY_DIRECTORY_RESOURCES_DIR_NAME, allocator); + std::string putenv_input = ROS_SECURITY_ROOT_DIRECTORY_VAR_NAME "="; + putenv_input += base_lookup_dir_fqn; + memcpy(g_envstring, putenv_input.c_str(), sizeof(g_envstring) - 1); + putenv_wrapper(g_envstring); + /* -------------------------- + * Namespace : Root + * Match type : Exact + * -------------------------- + * Root: ${CMAKE_BINARY_DIR}/tests/resources/test_security_directory + * Namespace: / + * Node: dummy_node */ + ASSERT_STREQ(rcl_get_secure_root(TEST_NODE_NAME, ROOT_NAMESPACE, + &allocator), + secure_root.c_str()); + putenv_wrapper(ROS_SECURITY_LOOKUP_TYPE_VAR_NAME "=MATCH_EXACT"); + ASSERT_STREQ(rcl_get_secure_root(TEST_NODE_NAME, ROOT_NAMESPACE, + &allocator), + secure_root.c_str()); + + /* -------------------------- + * Namespace : Root + * Match type : Prefix + * -------------------------- + * Root dir: ${CMAKE_BINARY_DIR}/tests/resources/test_security_directory + * Namespace: / + * Node: dummy_node_and_some_suffix_added */ + ASSERT_STRNE(rcl_get_secure_root(TEST_NODE_NAME "_and_some_suffix_added", ROOT_NAMESPACE, + &allocator), + secure_root.c_str()); + putenv_wrapper(ROS_SECURITY_LOOKUP_TYPE_VAR_NAME "=MATCH_PREFIX"); + ASSERT_STREQ(rcl_get_secure_root(TEST_NODE_NAME "_and_some_suffix_added", ROOT_NAMESPACE, + &allocator), + secure_root.c_str()); +} + +TEST_F(TestGetSecureRoot, nodeSecurityDirectoryOverride) { + rcl_allocator_t allocator = rcl_get_default_allocator(); + /* Specify a valid directory */ + putenv_wrapper(ROS_SECURITY_NODE_DIRECTORY_VAR_NAME "=" TEST_RESOURCES_DIRECTORY); + ASSERT_STREQ(rcl_get_secure_root("name shouldn't matter", "namespace shouldn't matter", + &allocator), TEST_RESOURCES_DIRECTORY); + + /* Setting root dir has no effect */ + putenv_wrapper(ROS_SECURITY_ROOT_DIRECTORY_VAR_NAME "=" TEST_RESOURCES_DIRECTORY); + ASSERT_STREQ(rcl_get_secure_root("name shouldn't matter", "namespace shouldn't matter", + &allocator), TEST_RESOURCES_DIRECTORY); + + /* The override provided should exist. Providing correct node/namespace/root dir won't help + * if the node override is invalid. */ + putenv_wrapper( + ROS_SECURITY_NODE_DIRECTORY_VAR_NAME + "=TheresN_oWayThi_sDirectory_Exists_hence_this_would_fail"); + ASSERT_EQ(rcl_get_secure_root(TEST_NODE_NAME, TEST_NODE_NAMESPACE, &allocator), + (char *) NULL); +} diff --git a/rcl/test/resources/test_security_directory/dummy_node/.gitkeep b/rcl/test/resources/test_security_directory/dummy_node/.gitkeep new file mode 100644 index 000000000..e69de29bb