Skip to content

Commit 57399fc

Browse files
Copilotbbockelm
andauthored
Add environment variable configuration loading on library initialization (#190)
* Implement environment-based global configuration for library - Add support for reading configuration from environment variables on library load - Environment variable format: SCITOKEN_CONFIG_<KEY> where <KEY> maps to config keys - Support case-insensitive environment variable names - Automatically detect and parse int vs string configuration values - Add comprehensive tests for environment variable configuration * Replace shared_ptr with mutex-protected strings for thread safety - Replace std::shared_ptr<std::string> with mutex-protected std::string - Add atomic flags for fast-path checking to avoid lock contention - Ensures thread-safe access to cache_home and tls_ca_file configuration --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Brian Bockelman <bbockelman@morgridge.org>
1 parent cb1463f commit 57399fc

File tree

6 files changed

+418
-17
lines changed

6 files changed

+418
-17
lines changed

src/scitokens.cpp

Lines changed: 87 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
#include <algorithm>
2+
#include <array>
13
#include <atomic>
4+
#include <cctype>
5+
#include <cstdlib>
26
#include <exception>
7+
#include <stdexcept>
38
#include <string.h>
49
#include <sys/stat.h>
510

@@ -10,15 +15,89 @@
1015
* GLOBALS
1116
*/
1217

13-
// Cache timeout config
14-
std::atomic_int configurer::Configuration::m_next_update_delta{600};
15-
std::atomic_int configurer::Configuration::m_expiry_delta{4 * 24 * 3600};
18+
// These are kept for backwards compatibility but are now handled by
19+
// construct-on-first-use in the Configuration class accessor functions
20+
// See scitokens_internal.h for the new implementation
21+
std::atomic_int configurer::Configuration::m_next_update_delta{0};
22+
std::atomic_int configurer::Configuration::m_expiry_delta{0};
23+
std::shared_ptr<std::string> configurer::Configuration::m_cache_home;
24+
std::shared_ptr<std::string> configurer::Configuration::m_tls_ca_file;
1625

17-
// SciTokens cache home config
18-
std::shared_ptr<std::string> configurer::Configuration::m_cache_home =
19-
std::make_shared<std::string>("");
20-
std::shared_ptr<std::string> configurer::Configuration::m_tls_ca_file =
21-
std::make_shared<std::string>("");
26+
namespace {
27+
28+
// Helper function to convert string to lowercase
29+
std::string to_lowercase(const std::string &str) {
30+
std::string result = str;
31+
std::transform(result.begin(), result.end(), result.begin(),
32+
[](unsigned char c) { return std::tolower(c); });
33+
return result;
34+
}
35+
36+
// Load configuration from environment variables on library initialization
37+
void load_config_from_environment() {
38+
// List of known configuration keys with their types and corresponding env
39+
// var names
40+
struct ConfigMapping {
41+
const char *config_key;
42+
const char *env_var_suffix; // After SCITOKEN_CONFIG_
43+
bool is_int;
44+
};
45+
46+
const std::array<ConfigMapping, 6> known_configs = {
47+
{{"keycache.update_interval_s", "KEYCACHE_UPDATE_INTERVAL_S", true},
48+
{"keycache.expiration_interval_s", "KEYCACHE_EXPIRATION_INTERVAL_S",
49+
true},
50+
{"keycache.cache_home", "KEYCACHE_CACHE_HOME", false},
51+
{"tls.ca_file", "TLS_CA_FILE", false},
52+
{"monitoring.file", "MONITORING_FILE", false},
53+
{"monitoring.file_interval_s", "MONITORING_FILE_INTERVAL_S", true}}};
54+
55+
const char *prefix = "SCITOKEN_CONFIG_";
56+
57+
// Check each known configuration
58+
for (const auto &config : known_configs) {
59+
// Build the full environment variable name
60+
std::string env_var = prefix + std::string(config.env_var_suffix);
61+
62+
// Also try case variations (uppercase, lowercase, mixed)
63+
const char *env_value = std::getenv(env_var.c_str());
64+
if (!env_value) {
65+
// Try with lowercase
66+
std::string env_var_lower = to_lowercase(env_var);
67+
env_value = std::getenv(env_var_lower.c_str());
68+
}
69+
70+
if (!env_value) {
71+
continue; // Not set in environment
72+
}
73+
74+
char *err_msg = nullptr;
75+
if (config.is_int) {
76+
try {
77+
int value = std::stoi(env_value);
78+
scitoken_config_set_int(config.config_key, value, &err_msg);
79+
} catch (const std::invalid_argument &) {
80+
// Silently ignore invalid integer format during initialization
81+
} catch (const std::out_of_range &) {
82+
// Silently ignore out-of-range values during initialization
83+
}
84+
} else {
85+
scitoken_config_set_str(config.config_key, env_value, &err_msg);
86+
}
87+
88+
// Free error message if any (we ignore errors during initialization)
89+
if (err_msg) {
90+
free(err_msg);
91+
}
92+
}
93+
}
94+
95+
// Use constructor attribute to run on library load
96+
__attribute__((constructor)) void init_scitokens_config() {
97+
load_config_from_environment();
98+
}
99+
100+
} // anonymous namespace
22101

23102
// Monitoring file config (empty string means disabled)
24103
// Protected by mutex; atomic flag for fast-path check

src/scitokens_internal.cpp

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1171,7 +1171,9 @@ configurer::Configuration::set_cache_home(const std::string dir_path) {
11711171
// If setting to "", then we should treat as though it is unsetting the
11721172
// config
11731173
if (dir_path.length() == 0) { // User is configuring to empty string
1174-
m_cache_home = std::make_shared<std::string>(dir_path);
1174+
std::lock_guard<std::mutex> lock(get_cache_home_mutex());
1175+
get_cache_home_string() = dir_path;
1176+
get_cache_home_set().store(false, std::memory_order_relaxed);
11751177
return std::make_pair(true, "");
11761178
}
11771179

@@ -1194,20 +1196,38 @@ configurer::Configuration::set_cache_home(const std::string dir_path) {
11941196

11951197
// Now it exists and we can write to it, set the value and let
11961198
// scitokens_cache handle the rest
1197-
m_cache_home = std::make_shared<std::string>(cleaned_dir_path);
1199+
{
1200+
std::lock_guard<std::mutex> lock(get_cache_home_mutex());
1201+
get_cache_home_string() = cleaned_dir_path;
1202+
get_cache_home_set().store(true, std::memory_order_relaxed);
1203+
}
11981204
return std::make_pair(true, "");
11991205
}
12001206

12011207
void configurer::Configuration::set_tls_ca_file(const std::string ca_file) {
1202-
m_tls_ca_file = std::make_shared<std::string>(ca_file);
1208+
std::lock_guard<std::mutex> lock(get_tls_ca_file_mutex());
1209+
get_tls_ca_file_string() = ca_file;
1210+
get_tls_ca_file_set().store(!ca_file.empty(), std::memory_order_relaxed);
12031211
}
12041212

12051213
std::string configurer::Configuration::get_cache_home() {
1206-
return *m_cache_home;
1214+
// Fast path: check if the value has been set
1215+
if (!get_cache_home_set().load(std::memory_order_relaxed)) {
1216+
return "";
1217+
}
1218+
// Slow path: acquire lock and read the value
1219+
std::lock_guard<std::mutex> lock(get_cache_home_mutex());
1220+
return get_cache_home_string();
12071221
}
12081222

12091223
std::string configurer::Configuration::get_tls_ca_file() {
1210-
return *m_tls_ca_file;
1224+
// Fast path: check if the value has been set
1225+
if (!get_tls_ca_file_set().load(std::memory_order_relaxed)) {
1226+
return "";
1227+
}
1228+
// Slow path: acquire lock and read the value
1229+
std::lock_guard<std::mutex> lock(get_tls_ca_file_mutex());
1230+
return get_tls_ca_file_string();
12111231
}
12121232

12131233
// bool configurer::Configuration::check_dir(const std::string dir_path) {

src/scitokens_internal.h

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,13 @@ class Configuration {
3737
public:
3838
Configuration() {}
3939
static void set_next_update_delta(int _next_update_delta) {
40-
m_next_update_delta = _next_update_delta;
40+
get_next_update_delta_ref() = _next_update_delta;
4141
}
42-
static int get_next_update_delta() { return m_next_update_delta; }
42+
static int get_next_update_delta() { return get_next_update_delta_ref(); }
4343
static void set_expiry_delta(int _expiry_delta) {
44-
m_expiry_delta = _expiry_delta;
44+
get_expiry_delta_ref() = _expiry_delta;
4545
}
46-
static int get_expiry_delta() { return m_expiry_delta; }
46+
static int get_expiry_delta() { return get_expiry_delta_ref(); }
4747
static std::pair<bool, std::string>
4848
set_cache_home(const std::string cache_home);
4949
static std::string get_cache_home();
@@ -61,6 +61,45 @@ class Configuration {
6161
}
6262

6363
private:
64+
// Accessor functions for construct-on-first-use idiom
65+
static std::atomic_int &get_next_update_delta_ref() {
66+
static std::atomic_int instance{600};
67+
return instance;
68+
}
69+
static std::atomic_int &get_expiry_delta_ref() {
70+
static std::atomic_int instance{4 * 24 * 3600};
71+
return instance;
72+
}
73+
74+
// Thread-safe accessors for string configurations
75+
static std::mutex &get_cache_home_mutex() {
76+
static std::mutex instance;
77+
return instance;
78+
}
79+
static std::string &get_cache_home_string() {
80+
static std::string instance;
81+
return instance;
82+
}
83+
static std::atomic<bool> &get_cache_home_set() {
84+
static std::atomic<bool> instance{false};
85+
return instance;
86+
}
87+
88+
static std::mutex &get_tls_ca_file_mutex() {
89+
static std::mutex instance;
90+
return instance;
91+
}
92+
static std::string &get_tls_ca_file_string() {
93+
static std::string instance;
94+
return instance;
95+
}
96+
static std::atomic<bool> &get_tls_ca_file_set() {
97+
static std::atomic<bool> instance{false};
98+
return instance;
99+
}
100+
101+
// Keep old declarations for backwards compatibility (will forward to
102+
// accessor functions)
64103
static std::atomic_int m_next_update_delta;
65104
static std::atomic_int m_expiry_delta;
66105
static std::shared_ptr<std::string> m_cache_home;

test/CMakeLists.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,22 @@ add_test(
2020
${CMAKE_CURRENT_BINARY_DIR}/scitokens-gtest
2121
)
2222

23+
# Environment variable configuration test executable
24+
add_executable(scitokens-env-test test_env_config.cpp)
25+
target_link_libraries(scitokens-env-test SciTokens)
26+
27+
# Environment variable configuration test
28+
add_test(
29+
NAME
30+
env_config
31+
COMMAND
32+
${CMAKE_CURRENT_BINARY_DIR}/scitokens-env-test
33+
)
34+
35+
set_tests_properties(env_config
36+
PROPERTIES
37+
ENVIRONMENT "SCITOKEN_CONFIG_KEYCACHE_UPDATE_INTERVAL_S=1234;SCITOKEN_CONFIG_KEYCACHE_EXPIRATION_INTERVAL_S=5678;SCITOKEN_CONFIG_KEYCACHE_CACHE_HOME=/tmp/test_scitoken_cache;SCITOKEN_CONFIG_TLS_CA_FILE=/tmp/test_ca.pem"
38+
)
2339
# Monitoring unit tests
2440
add_executable(scitokens-monitoring-test monitoring_test.cpp)
2541
if( NOT SCITOKENS_EXTERNAL_GTEST )

test/main.cpp

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,91 @@ TEST_F(IssuerSecurityTest, SpecialCharacterIssuer) {
943943
EXPECT_NE(error_message.find("\""), std::string::npos);
944944
}
945945

946+
// Test suite for environment variable configuration
947+
class EnvConfigTest : public ::testing::Test {
948+
protected:
949+
void SetUp() override {
950+
// Save original config values
951+
char *err_msg = nullptr;
952+
original_update_interval =
953+
scitoken_config_get_int("keycache.update_interval_s", &err_msg);
954+
original_expiry_interval =
955+
scitoken_config_get_int("keycache.expiration_interval_s", &err_msg);
956+
957+
char *cache_home = nullptr;
958+
scitoken_config_get_str("keycache.cache_home", &cache_home, &err_msg);
959+
if (cache_home) {
960+
original_cache_home = cache_home;
961+
free(cache_home);
962+
}
963+
964+
char *ca_file = nullptr;
965+
scitoken_config_get_str("tls.ca_file", &ca_file, &err_msg);
966+
if (ca_file) {
967+
original_ca_file = ca_file;
968+
free(ca_file);
969+
}
970+
}
971+
972+
void TearDown() override {
973+
// Restore original config values
974+
char *err_msg = nullptr;
975+
scitoken_config_set_int("keycache.update_interval_s",
976+
original_update_interval, &err_msg);
977+
scitoken_config_set_int("keycache.expiration_interval_s",
978+
original_expiry_interval, &err_msg);
979+
scitoken_config_set_str("keycache.cache_home",
980+
original_cache_home.c_str(), &err_msg);
981+
scitoken_config_set_str("tls.ca_file", original_ca_file.c_str(),
982+
&err_msg);
983+
}
984+
985+
int original_update_interval = 600;
986+
int original_expiry_interval = 4 * 24 * 3600;
987+
std::string original_cache_home;
988+
std::string original_ca_file;
989+
};
990+
991+
TEST_F(EnvConfigTest, IntConfigFromEnv) {
992+
// Note: This test verifies that the environment variable was read at
993+
// library load time We can't test setting environment variables after
994+
// library load in the same process This test would need to be run with
995+
// environment variables set before starting the test
996+
997+
// Test that we can manually set and get config values
998+
char *err_msg = nullptr;
999+
int test_value = 1234;
1000+
auto rv = scitoken_config_set_int("keycache.update_interval_s", test_value,
1001+
&err_msg);
1002+
ASSERT_EQ(rv, 0) << (err_msg ? err_msg : "");
1003+
1004+
int retrieved =
1005+
scitoken_config_get_int("keycache.update_interval_s", &err_msg);
1006+
EXPECT_EQ(retrieved, test_value) << (err_msg ? err_msg : "");
1007+
1008+
if (err_msg)
1009+
free(err_msg);
1010+
}
1011+
1012+
TEST_F(EnvConfigTest, StringConfigFromEnv) {
1013+
// Test that we can manually set and get string config values
1014+
char *err_msg = nullptr;
1015+
const char *test_path = "/tmp/test_cache";
1016+
auto rv =
1017+
scitoken_config_set_str("keycache.cache_home", test_path, &err_msg);
1018+
ASSERT_EQ(rv, 0) << (err_msg ? err_msg : "");
1019+
1020+
char *output = nullptr;
1021+
rv = scitoken_config_get_str("keycache.cache_home", &output, &err_msg);
1022+
ASSERT_EQ(rv, 0) << (err_msg ? err_msg : "");
1023+
ASSERT_TRUE(output != nullptr);
1024+
EXPECT_STREQ(output, test_path);
1025+
1026+
free(output);
1027+
if (err_msg)
1028+
free(err_msg);
1029+
}
1030+
9461031
int main(int argc, char **argv) {
9471032
::testing::InitGoogleTest(&argc, argv);
9481033
return RUN_ALL_TESTS();

0 commit comments

Comments
 (0)