Skip to content

Commit d4e7332

Browse files
Copilotbbockelm
authored andcommitted
Add monitoring API infrastructure
1 parent 1e3553c commit d4e7332

File tree

8 files changed

+679
-117
lines changed

8 files changed

+679
-117
lines changed

CMakeLists.txt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ pkg_check_modules(SQLITE REQUIRED sqlite3)
4444

4545
endif()
4646

47-
add_library(SciTokens SHARED src/scitokens.cpp src/scitokens_internal.cpp src/scitokens_cache.cpp)
47+
add_library(SciTokens SHARED src/scitokens.cpp src/scitokens_internal.cpp src/scitokens_cache.cpp src/scitokens_monitoring.cpp)
4848
target_compile_features(SciTokens PUBLIC cxx_std_11) # Use at least C++11 for building and when linking to scitokens
4949
target_include_directories(SciTokens PUBLIC ${JWT_CPP_INCLUDES} "${PROJECT_SOURCE_DIR}/src" PRIVATE ${CURL_INCLUDE_DIRS} ${OPENSSL_INCLUDE_DIRS} ${LIBCRYPTO_INCLUDE_DIRS} ${SQLITE_INCLUDE_DIRS} ${UUID_INCLUDE_DIRS})
5050

@@ -75,10 +75,17 @@ target_link_libraries(scitokens-list-access SciTokens)
7575
add_executable(scitokens-create src/create.cpp)
7676
target_link_libraries(scitokens-create SciTokens)
7777

78+
7879
add_executable(scitokens-generate-jwks src/generate_jwks.cpp)
7980
target_include_directories(scitokens-generate-jwks PRIVATE ${OPENSSL_INCLUDE_DIRS} ${LIBCRYPTO_INCLUDE_DIRS})
8081
target_link_libraries(scitokens-generate-jwks ${OPENSSL_LIBRARIES} ${LIBCRYPTO_LIBRARIES})
8182

83+
add_executable(scitokens-test-monitoring src/test_monitoring.cpp)
84+
target_link_libraries(scitokens-test-monitoring SciTokens)
85+
86+
add_executable(scitokens-test-monitoring-comprehensive src/test_monitoring_comprehensive.cpp)
87+
target_link_libraries(scitokens-test-monitoring-comprehensive SciTokens)
88+
8289
get_directory_property(TARGETS BUILDSYSTEM_TARGETS)
8390
install(
8491
TARGETS ${TARGETS}

src/scitokens.cpp

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -246,10 +246,12 @@ int scitoken_get_expiration(const SciToken token, long long *expiry,
246246
// Float value - convert to integer (truncate)
247247
// Float value - convert to integer using std::floor().
248248
// This ensures expiration is not extended by fractional seconds.
249-
result = static_cast<long long>(std::floor(claim_value.get<double>()));
249+
result =
250+
static_cast<long long>(std::floor(claim_value.get<double>()));
250251
} else {
251252
if (err_msg) {
252-
*err_msg = strdup("'exp' claim must be a number (integer or float)");
253+
*err_msg =
254+
strdup("'exp' claim must be a number (integer or float)");
253255
}
254256
return -1;
255257
}
@@ -1080,9 +1082,9 @@ int scitoken_config_set_str(const char *key, const char *value,
10801082
return -1;
10811083
}
10821084
} else if (_key == "tls.ca_file") {
1083-
configurer::Configuration::set_tls_ca_file(value ? std::string(value) : "");
1084-
}
1085-
else {
1085+
configurer::Configuration::set_tls_ca_file(value ? std::string(value)
1086+
: "");
1087+
} else {
10861088
if (err_msg) {
10871089
*err_msg = strdup("Key not recognized.");
10881090
}
@@ -1114,3 +1116,35 @@ int scitoken_config_get_str(const char *key, char **output, char **err_msg) {
11141116
}
11151117
return 0;
11161118
}
1119+
1120+
int scitoken_get_monitoring_json(char **json_out, char **err_msg) {
1121+
if (!json_out) {
1122+
if (err_msg) {
1123+
*err_msg = strdup("JSON output pointer may not be null.");
1124+
}
1125+
return -1;
1126+
}
1127+
try {
1128+
std::string json =
1129+
scitokens::internal::MonitoringStats::instance().get_json();
1130+
*json_out = strdup(json.c_str());
1131+
} catch (std::exception &exc) {
1132+
if (err_msg) {
1133+
*err_msg = strdup(exc.what());
1134+
}
1135+
return -1;
1136+
}
1137+
return 0;
1138+
}
1139+
1140+
int scitoken_reset_monitoring_stats(char **err_msg) {
1141+
try {
1142+
scitokens::internal::MonitoringStats::instance().reset();
1143+
} catch (std::exception &exc) {
1144+
if (err_msg) {
1145+
*err_msg = strdup(exc.what());
1146+
}
1147+
return -1;
1148+
}
1149+
return 0;
1150+
}

src/scitokens.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,27 @@ int scitoken_config_set_str(const char *key, const char *value, char **err_msg);
329329
*/
330330
int scitoken_config_get_str(const char *key, char **output, char **err_msg);
331331

332+
/**
333+
* Get monitoring statistics as a JSON string.
334+
* Returns a JSON object containing per-issuer validation statistics including:
335+
* - successful_validations: count of successful token validations
336+
* - unsuccessful_validations: count of failed token validations
337+
* - expired_tokens: count of expired tokens encountered
338+
* - total_validation_time_s: total validation time in seconds
339+
* - failed_issuer_lookups: count of failed issuer lookups (limited to prevent
340+
* DDoS)
341+
*
342+
* The returned string must be freed by the caller using free().
343+
* Returns 0 on success, nonzero on failure.
344+
*/
345+
int scitoken_get_monitoring_json(char **json_out, char **err_msg);
346+
347+
/**
348+
* Reset all monitoring statistics.
349+
* Returns 0 on success, nonzero on failure.
350+
*/
351+
int scitoken_reset_monitoring_stats(char **err_msg);
352+
332353
#ifdef __cplusplus
333354
}
334355
#endif

src/scitokens_internal.cpp

Lines changed: 115 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -652,116 +652,133 @@ SciToken::deserialize_continue(std::unique_ptr<SciTokenAsyncStatus> status) {
652652
std::unique_ptr<AsyncStatus>
653653
Validator::get_public_keys_from_web(const std::string &issuer,
654654
unsigned timeout) {
655-
std::string openid_metadata, oauth_metadata;
656-
get_metadata_endpoint(issuer, openid_metadata, oauth_metadata);
657-
658-
std::unique_ptr<AsyncStatus> status(new AsyncStatus());
659-
status->m_oauth_metadata_url = oauth_metadata;
660-
status->m_cget.reset(new internal::SimpleCurlGet(1024 * 1024, timeout));
661-
auto cget_status = status->m_cget->perform_start(openid_metadata);
662-
status->m_continue_fetch = true;
663-
if (!cget_status.m_done) {
664-
return status;
665-
}
666-
return get_public_keys_from_web_continue(std::move(status));
655+
try {
656+
std::string openid_metadata, oauth_metadata;
657+
get_metadata_endpoint(issuer, openid_metadata, oauth_metadata);
658+
659+
std::unique_ptr<AsyncStatus> status(new AsyncStatus());
660+
status->m_oauth_metadata_url = oauth_metadata;
661+
status->m_cget.reset(new internal::SimpleCurlGet(1024 * 1024, timeout));
662+
auto cget_status = status->m_cget->perform_start(openid_metadata);
663+
status->m_continue_fetch = true;
664+
if (!cget_status.m_done) {
665+
return status;
666+
}
667+
return get_public_keys_from_web_continue(std::move(status));
668+
} catch (const CurlException &e) {
669+
// Rethrow CURL errors during issuer key fetch as IssuerLookupException
670+
throw IssuerLookupException(e.what());
671+
}
667672
}
668673

669674
std::unique_ptr<AsyncStatus> Validator::get_public_keys_from_web_continue(
670675
std::unique_ptr<AsyncStatus> status) {
671-
char *buffer;
672-
size_t len;
676+
try {
677+
char *buffer;
678+
size_t len;
673679

674-
switch (status->m_state) {
680+
switch (status->m_state) {
675681

676-
case AsyncStatus::DOWNLOAD_METADATA: {
677-
auto cget_status = status->m_cget->perform_continue();
678-
if (!cget_status.m_done) {
679-
return std::move(status);
680-
}
681-
if (cget_status.m_status_code != 200) {
682-
if (status->m_oauth_fallback) {
683-
throw CurlException("Failed to retrieve metadata provider "
684-
"information for issuer.");
685-
} else {
686-
status->m_oauth_fallback = true;
687-
status->m_cget.reset(new internal::SimpleCurlGet());
688-
cget_status =
689-
status->m_cget->perform_start(status->m_oauth_metadata_url);
690-
if (!cget_status.m_done) {
691-
return std::move(status);
682+
case AsyncStatus::DOWNLOAD_METADATA: {
683+
auto cget_status = status->m_cget->perform_continue();
684+
if (!cget_status.m_done) {
685+
return std::move(status);
686+
}
687+
if (cget_status.m_status_code != 200) {
688+
if (status->m_oauth_fallback) {
689+
throw IssuerLookupException(
690+
"Failed to retrieve metadata provider "
691+
"information for issuer.");
692+
} else {
693+
status->m_oauth_fallback = true;
694+
status->m_cget.reset(new internal::SimpleCurlGet());
695+
cget_status = status->m_cget->perform_start(
696+
status->m_oauth_metadata_url);
697+
if (!cget_status.m_done) {
698+
return std::move(status);
699+
}
700+
return get_public_keys_from_web_continue(std::move(status));
692701
}
693-
return get_public_keys_from_web_continue(std::move(status));
694702
}
703+
status->m_cget->get_data(buffer, len);
704+
std::string metadata(buffer, len);
705+
picojson::value json_obj;
706+
auto err = picojson::parse(json_obj, metadata);
707+
if (!err.empty()) {
708+
throw JsonException("JSON parse failure when downloading from "
709+
"the metadata URL " +
710+
status->m_cget->get_url() + ": " + err);
711+
}
712+
if (!json_obj.is<picojson::object>()) {
713+
throw JsonException("Metadata resource " +
714+
status->m_cget->get_url() +
715+
" contains "
716+
"improperly-formatted JSON.");
717+
}
718+
auto top_obj = json_obj.get<picojson::object>();
719+
auto iter = top_obj.find("jwks_uri");
720+
if (iter == top_obj.end() || (!iter->second.is<std::string>())) {
721+
throw JsonException("Metadata resource " +
722+
status->m_cget->get_url() +
723+
" is missing 'jwks_uri' string value");
724+
}
725+
auto jwks_uri = iter->second.get<std::string>();
726+
status->m_has_metadata = true;
727+
status->m_state = AsyncStatus::DOWNLOAD_PUBLIC_KEY;
728+
status->m_cget.reset(new internal::SimpleCurlGet());
729+
status->m_cget->perform_start(jwks_uri);
730+
// This should also fall through the next state
695731
}
696-
status->m_cget->get_data(buffer, len);
697-
std::string metadata(buffer, len);
698-
picojson::value json_obj;
699-
auto err = picojson::parse(json_obj, metadata);
700-
if (!err.empty()) {
701-
throw JsonException(
702-
"JSON parse failure when downloading from the metadata URL " +
703-
status->m_cget->get_url() + ": " + err);
704-
}
705-
if (!json_obj.is<picojson::object>()) {
706-
throw JsonException("Metadata resource " +
707-
status->m_cget->get_url() +
708-
" contains "
709-
"improperly-formatted JSON.");
710-
}
711-
auto top_obj = json_obj.get<picojson::object>();
712-
auto iter = top_obj.find("jwks_uri");
713-
if (iter == top_obj.end() || (!iter->second.is<std::string>())) {
714-
throw JsonException("Metadata resource " +
715-
status->m_cget->get_url() +
716-
" is missing 'jwks_uri' string value");
717-
}
718-
auto jwks_uri = iter->second.get<std::string>();
719-
status->m_has_metadata = true;
720-
status->m_state = AsyncStatus::DOWNLOAD_PUBLIC_KEY;
721-
status->m_cget.reset(new internal::SimpleCurlGet());
722-
status->m_cget->perform_start(jwks_uri);
723-
// This should also fall through the next state
724-
}
725732

726-
case AsyncStatus::DOWNLOAD_PUBLIC_KEY: {
727-
auto cget_status = status->m_cget->perform_continue();
728-
if (!cget_status.m_done) {
729-
return std::move(status);
730-
}
731-
if (cget_status.m_status_code != 200) {
732-
throw CurlException("Failed to retrieve the issuer's key set");
733-
}
733+
case AsyncStatus::DOWNLOAD_PUBLIC_KEY: {
734+
auto cget_status = status->m_cget->perform_continue();
735+
if (!cget_status.m_done) {
736+
return std::move(status);
737+
}
738+
if (cget_status.m_status_code != 200) {
739+
throw IssuerLookupException(
740+
"Failed to retrieve the issuer's key set");
741+
}
734742

735-
status->m_cget->get_data(buffer, len);
736-
auto metadata = std::string(buffer, len);
737-
picojson::value json_obj;
738-
auto err = picojson::parse(json_obj, metadata);
739-
if (!err.empty()) {
740-
throw JsonException("JSON parse failure when downloading from the "
741-
" public key URL " +
742-
status->m_cget->get_url() + ": " + err);
743+
status->m_cget->get_data(buffer, len);
744+
auto metadata = std::string(buffer, len);
745+
picojson::value json_obj;
746+
auto err = picojson::parse(json_obj, metadata);
747+
if (!err.empty()) {
748+
throw JsonException(
749+
"JSON parse failure when downloading from the "
750+
" public key URL " +
751+
status->m_cget->get_url() + ": " + err);
752+
}
753+
status->m_cget.reset();
754+
755+
auto now = std::time(NULL);
756+
// TODO: take expiration time from the cache-control header in the
757+
// response.
758+
759+
int next_update_delta =
760+
configurer::Configuration::get_next_update_delta();
761+
int expiry_delta = configurer::Configuration::get_expiry_delta();
762+
status->m_next_update = now + next_update_delta;
763+
status->m_expires = now + expiry_delta;
764+
status->m_keys = json_obj;
765+
status->m_continue_fetch = false;
766+
status->m_done = true;
767+
status->m_state = AsyncStatus::DONE;
743768
}
744-
status->m_cget.reset();
745-
746-
auto now = std::time(NULL);
747-
// TODO: take expiration time from the cache-control header in the
748-
// response.
749-
750-
int next_update_delta =
751-
configurer::Configuration::get_next_update_delta();
752-
int expiry_delta = configurer::Configuration::get_expiry_delta();
753-
status->m_next_update = now + next_update_delta;
754-
status->m_expires = now + expiry_delta;
755-
status->m_keys = json_obj;
756-
status->m_continue_fetch = false;
757-
status->m_done = true;
758-
status->m_state = AsyncStatus::DONE;
759-
}
760-
case AsyncStatus::DONE:
761-
status->m_done = true;
762-
763-
} // Switch
764-
return std::move(status);
769+
case AsyncStatus::DONE:
770+
status->m_done = true;
771+
772+
} // Switch
773+
return std::move(status);
774+
} catch (const CurlException &e) {
775+
// Rethrow CURL errors during issuer key fetch as IssuerLookupException
776+
// (unless it's already an IssuerLookupException)
777+
if (dynamic_cast<const IssuerLookupException *>(&e)) {
778+
throw;
779+
}
780+
throw IssuerLookupException(e.what());
781+
}
765782
}
766783

767784
std::string Validator::get_jwks(const std::string &issuer) {

0 commit comments

Comments
 (0)